قمنا في الدرس السابق بالتعرف على محرك Unity وكيفية تحميله وإنشاء مشروع جديد عليه، سنبدأ في هذا الدرس في مشروع عملي لتعلم كيفية صناعة لعبة حقيقية كاملة. سنتكلم في الدرس الأول عن إعداد مشهد اللعبة والتحكم بالكاميرا.
ملاحظة: يمكن تحميل الملفات المصدرية لكامل هذه السلسلة عبر حساب أكاديمية حسوب على Github، يمكن أيضا تحميل ملف APK لتجريب اللعبة على أجهزة Android.
استيراد الصور وتجهيزها
سأستخدم في هذا المثال مجموعة من الرسوم المطروحة مجانا تحت رخصة المشاع الإبداعي CC0 وهي متوفرة على الموقع http://kenney.nl. المجموعة الأولى عبارة عن رسومات خاصة بالألعاب الفيزيائية مثل Angry Birds، وهي اللعبة التي نحاول أن نحاكي ميكانيكياتها عبر هذه السلسلة. أما المجموعة الثانية فهي عبارة عن رسوم حيوانات بسيطة سنستخدمها بدلا عن الطيور الموجودة في اللعبة الأصلية بحيث تمثل المقذوفات. إلا أننا سنضيف بعض الرسومات الأخرى وملفات الأصوات حسب الحاجة. سأعتمد في هذه السلسلة وضع كل مصدر (ملف صورة أو ملف صوت) داخل مجلد يحمل اسم الموقع الذي تم جلب المصدر منه، بحيث يمكنك الرجوع لاحقا لهذه المواقع وتحميل الملفات.
بعد تحميل هذه الرسومات وفك ضغطها قم بنسخها إلى داخل المجلد Assets الخاص بمشروعك، يمكنك نسخها عن طريق نظام التشغيل أو بسحبها مباشرة إلى داخل المجلد في مستعرض Unity. بمجرد أن تضيف هذه الملفات للمشروع سيتعرف عليها Unity على أنها رسومات شبحية sprites، وهي أنواع الرسوم الأكثر شيوعا في الألعاب ثنائية الأبعاد. من أجل الحصول على أعلى جودة ممكنة سنقوم بتعديل بعض خصائص الرسومات، سأضرب مثالا هنا على مجلد واحد ومن ثم علينا تكرار العملية لجميع مجلدات الرسومات.
ملاحظة: تحتوي مجموعات الرسومات على طريقتين مختلفتين للتخزين: الأولى تخزين كل صورة على حدى في ملف مستقل، والثانية جمع الصور في ملف واحد يسمى ورقة الرسومات sprite sheet. سنتناول في هذه الدروس الطريقة الأولى وهي الأشكال المنفصلة؛ وذلك لسهولة العمل عليها. علما بأن الطريقة الثانية أكثر كفاءة من ناحية الأداء وأسهل في حالة الرسومات المتحركة.
كما ترى في الشكل التالي فإنني قمت باختيار جميع عناصر مجلد رسومات الوحوش والتي ستمثل الخصوم في هذه اللعبة.
بعد ذلك قمت بتعديل خصائص استيراد هذه الرسومات لتصبح كالتالي:
بتغيير نوع المرشِّح إلى Trilinear وتنسيق الألوان إلى Truecolor فإننا نطلب من Unity ألا يقوم بضغط هذه الصور بحيث تبقى بحجمها الأصلي وتحافظ على جودتها. على الرغم من أن الضغط أفضل للأداء، إلا أنك ستلاحظ أن Unity يخبرك أن معظم هذه الرسومات غير قابلة للضغط كونها لا تحقق قاعدة أن يكون الطول والعرض مساويين لأرقام لوغاريتمية للأساس 2 (مثلا 64، 128، 512، إلخ). لا تنس أن تضغط على Apply لحفظ التغييرات بعد الانتهاء منها.
بناء المشهد وتجهيز الخلفية
بعد تغيير الخصائص لجميع الرسومات التي استوردناها يمكننا أن نبدأ بإعداد المشهد. الخطوة الأولى ستكون بإضافة الخلفية والأرضية التي ستدور أحداث اللعبة عليها. إحدى المواصفات المهمة في عملية الاستيراد هي Pixels Per Unit. الرقم 100 هنا يعني أن خطا بطول 100 بكسل في الصورة الأصلية يغطي مسافة قدرها وحدة واحدة في فضاء Unity. فلو افترضت أن الوحدة هي متر واحد، فإن صورة صندوق بحجم 10×10 بكسل تساوي صندوقا بحجم 10×10 سم في فضاء اللعبة. لحسن حظنا فإن مجموعة الصور التي استوردناها ذات أحجام مناسبة لبعضها البعض، فمثلا صور الوحوش هي ذات مقاس 70×70 بكسل بينما الخلفيات أكبر من ذلك بكثير وتصل إلى 1024×1024 بكسل.
لنأخذ بعين الاعتبار منذ البداية أن هذه السلسلة ليست لتعلم برمجة الألعاب فقط، كما أنها ليست لتعلم محرك Unity فقط، بل يمكنك القول أنها ستعلمك أيضا أفضل الممارسات البرمجية والتنظيمية للمشهد لتحصل على أفضل نتيجة باستخدام هذا المحرك. من أجل ذلك سنقوم بإعداد مشهد اللعبة بطريقة خاصة بحيث يحتوي المشهد على كائن واحد نسميه الكائن الجذري للمشهد SceneRoot ومن ثم سنضيف كل عناصر المشهد على شكل أبناء وأحفاد لهذا الكائن. لإضافة كائن جديد كل ما عليك هو اختيار القائمة:
Game Object > Create Empty
وسيظهر بعدها الكائن الجديد في هرمية المشهد باسم GameObject، قم باختياره وتغيير اسمه إلى SceneRoot وموقعه إلى نقطة الأصل (0 ,0). كما ذكرت في مقدمة هذا الفصل يمكنك تغيير هذه القيم من نافذة الخصائص Inspector كما هو موضح في الصورة أدناه. لاحظ أن هذا الكائن "فارغ" كما يدل اسمه، أي أنه موجود في الذاكرة فقط ولا يمكن رؤيته في المشهد رغم أنه موجود في وسطه، سنأتي إلى هذه التفاصيل حسب حاجتنا لها.
بعد ذلك سنضيف كائنا فارغا آخر ليمثل خلفية المشهد، كل ما عليك هو أن تضغط بزر الفأرة الأيمن على الكائن SceneRoot في الهرمية وتختار Create Empty من أجل أن تضيف كائنا فرعيا (ابنا) للكائن الجذري ولنقم بتسميته Background. أخيرا عليك أن تقوم بسحب كائن الكاميرا Main Camera إلى داخل الكائن الجذري بحيث تصبح هرمية المشهد بهذا الشكل.
حان الوقت الآن لحفظ المشهد الذي نعمل عليه، كما هو الحال في 99% من البرامج يمكنك الحفظ عن طريق Control+S وهو أمر خاص بحفظ المشهد وليس حفظ المشروع كاملا، حيث أن المشروع ما هو إلا مجموعة ملفات يتم تعديلها وحفظها على حدى. قم بإنشاء مجلد جديد داخل Assets وقم بتسميته Scenes ومن ثم احفظ المشهد داخله باسم GameScene.
لنتفق من الآن على أننا سنقوم ببناء مشهد واحد لكل اللعبة، وسنعمل على تغيير كل من الخلفية والأرضية ومواقع الكائنات لصنع مراحل مختلفة. الكائن الأول سيكون خلفية المشهد، والذي توجد عدة صور يمكن استخدامها له. هذه الصور عددها 8 وهي موجودة في المجلد Backgrounds في مجموعة الصور الأولى كما ترى في الصورة أدناه. ما يميز صور الخلفيات هذه هو أن كلا منها ينطبق طرفها الأيمن على طرفها الأيسر تماما مما يجعلنا قادرين على تكرارها أفقيا بشكل متتابع دون أن يلاحظ اللاعب وجود حدود بينها.
بما أننا نتعامل مع لعبة ذات مشهد عرضي فإن علينا أن نكرر الخلفية أكثر من مرة بشكل أفقي، ولتكن 4 مرات. قبل أن نبدأ بإضافة الخلفيات للمشهد، لنسهل الأمر على أنفسنا بجعل نقطة الارتكاز Pivot لجميع صور الخلفيات هي الطرف الأيسر من الصورة بدل الأوسط، وذلك كما ترى في الصورة التالية:
ولكن ماذا يعني أن تكون نقطة ارتكاز الصورة هي طرفها الأيسر وليس وسطها؟ الصورتان في الأسفل تجيبان على هذا السؤال.
في كلتا الحالتين فإن موقع الصورة لم يتغير وهو (0 ,0) أي في منتصف المشهد. في الوضع الافتراضي (الصورة اليسرى) فإن ارتكاز الصورة يكون في منتصفها، أي أن موقع الصورة في الفضاء هو موقع منتصفها، ويمتد طرفا الصورة الأيسر والأيمن بين النقطتين x=-5.12 و x=+5.12. أما في الحالة الثانية قمنا بتغيير الارتكاز لأقصى يسار الصورة أفقيا مع بقائه في منتصفها عموديا. بهذا الشكل أصبحت الصورة تمتد من النقطة x=0 يسارا إلى النقطة x=+10.24 يمينا. لاحظ أن قيم هذه النقاط متوافقة مع حقيقة أن الوحدة الواحدة =100 بكسل حيث يكون عرض الصورة بالوحدات هو 10.2=1024/100
بهذا أصبح بإمكاننا أن نحسب مواقع الصور الأربعة التي سنقوم برصّها بجانب بعضها البعض من اليسار لليمين حول نقطة الأصل، وهي كالتالي:
- الصورة في أقصى اليسار ستكون في الموقع 20.48- وتمتد إلى 10.24-.
- الصورة على يسار نقطة الأصل ستكون في الموقع 10.24- وتمتد إلى 0.0.
- الصورة على يمين نقطة الأصل ستكون في الموقع 0.0 وتمتد إلى 10.24+.
- الصورة في أقصى اليمين ستكون في الموقع 10.24+ وتمتد إلى 20.48+.
بدلا من أن نقوم بوضع هذه الصورة يدويا، سنقوم بكتابة بُريمج صغير script بحيث نعطيه صورة ونطلب منه أن يكررها عددا من المرات ليصنع لنا الخلفية. هذا البريمج سيعمل بالشكل التالي: سنقوم بإعطائه جميع صور الخلفيات الموجودة لدينا على شكل مصفوفة بحيث تحمل كل صورة رقما خاصا بها، إضافة لذلك سنكون قادرين على تزويده بعدد مرات التكرار المطلوبة للصورة من أجل رسم الخلفية. عند تشغيل اللعبة فإنه يجب علينا أن نقوم باختيار رقم الخلفية التي نرغب بظهورها على الشاشة، وهذا الرقم سيكون من 0 إلى 7. سنعود لموضوع البريمج هذا بعد قليل.
قبل الخوض في التفاصيل أود التنويه إلى أن الكائنات في محرك Unity جميعها تتشابه، وهي في وضعها الأصلي فارغة غير مرئية، وما يميز كل كائن عن غيره ويعطيه سماته الخاصة هو مجموعة المكوّنات التي تتم إضافتها إليه. المكوّن الوحيد الموجود في الكائن الفارغ هو Transform، وهو موجود في جميع كائنات المحرك دون استثناء، حيث أنه يعطي الكائن موقعه ودورانه وحجمه في الفضاء ثنائي الأبعاد (أو ثلاثي الأبعاد) إضافة إلى أنه يحدد علاقاته بالكائنات الأخرى كعلاقة الأب والأبناء التي نراها في هرمية المشهد. بناء على هذه الحقيقة، فإن ما تراه في شاشة الخصائص Inspector حين تختار أي كائن هو مجموعة المكوّنات المضافة إليه.
ما سنقوم به الآن هو بناء قالب prefab، وهو عبارة عن كائن نضيف إليه بعض المكوّنات، ومن ثم يمكننا إنشاء عدة نسخ منه أثناء تشغيل اللعبة. الأمر الجيد في هذه القوالب هو أنه يمكن تعديلها من مكان واحد مهما بلغ عدد الكائنات التي يتم إنشاؤها منها، حيث ينعكس أي تعديل في القالب الأصلي (كإضافة مكون أو حذفه) على جميع الكائنات التي تتبع هذا القالب. القالب الذي سنقوم به خاص بالكائن الذي سيعرض صورة الخلفية، والذي سيحتاج مبدئيا لمكوّن واحد وهو Sprite Renderer الخاص بتصيير الصور ثنائية الأبعاد على الشاشة. لإنشاء القالب لنقم أولا بإضافة كائن فارغ للمشهد ومن ثم نضيف له المكوّن المذكور وذلك عن طريق الزر Add Component والذي يعطيك قائمة بجميع المكوّنات المتوفرة، ولتسهيل الوصول يمكنك البحث عن طريق كتابة اسم المكوّن Sprite Renderer ومن ثم اختياره كما يلي:
بعد إضافة المكوّن أصبحنا جاهزين لتحويل هذا الكائن إلى قالب، وذلك عن طريق سحبه من الهرمية إلى داخل أي مجلد في المشروع، في هذه الحالة سأقوم بإنشاء مجلد جديد داخل Assets وأسميه Prefabs لأننا سنحتاج في هذا العمل إلى عدد كبير من القوالب كما سنرى. بعدها قم بتغيير اسم القالب ليصبح BGElement. بعد إنشاء القالب لم نعد بحاجة للكائن في المشهد بالتالي يمكننا حذفه باستخدام مفتاح Delete. انتبه هنا أنك تحذف الكائن من الهرمية ولا تحذف القالب من المشروع فهما يحملان نفس الاسم لكن الفرق كبير بين الأمرين!
لإضافة بريمج جديد للمشروع لنقم أولا بإنشاء مجلد خاص بالبريمجات داخل Assets ولنسمه Scripts. بعدها يمكنك إضافة بريمج جديد بلغة #C وذلك عن طريق الضغط على المجلد بزر الفأرة الأيمن ومن ثم اختيار:
Create > C# Script.
أدخل الاسم BackgroundManager ومن ثم اضغط Enter وبعدها قم بفتح الملف على محرر MonoDevelop أو Visual Studio وذلك بالنقر المزدوج. لنشاهد البريمج الذي سنكتبه في السرد التالي ثم نناقشه معا بالتفصيل.
using UnityEngine; using System.Collections; public class BackgroundManager : MonoBehaviour { //القالب المستخدم لبناء عناصر الخلفية public GameObject bgElementPrefab; //مصفوفة تحتوي على جميع صور الخلفيات المتاحة public Sprite[] selectionList; //عدد مرات تكرار صورة الخلفية public int repeatCount = 4; //صورة الخلفية المعروضة حاليا private int selectedIndex = -1; //"Background" مرجع للكائن الابن المسمى private Transform bgGameObject; //تستدعى مرة واحدة عند بداية التشغيل void Start () { //"Background" ابحث في الأبناء على الكائن الذي يحمل الاسم bgGameObject = transform.FindChild("Background"); //قم مبدئيا باختيار الصورة الأولى كخلفية ChangeBackground(0); } //تستدعى مرة واحدة عند تصيير كل إطار void Update () { } //تقوم بتغيير صورة الخلفية المعروضة حاليا public void ChangeBackground(int newIndex){ if(newIndex == selectedIndex){ //لا حاجة لتغيير الصورة return; } //قم بحذف صور الخلفية الحالية for(int i = 0; i < bgGameObject.childCount; i++){ Transform bgSprite = bgGameObject.GetChild(i); Destroy(bgSprite.gameObject); } //قم بتخزين صورة الخلفية الجديدة من المصفوفة حسب الموقع المعطى Sprite newSprite = selectionList[newIndex]; //ما هو عرض صورة الخلفية بالبكسل؟ float width = newSprite.rect.width; //ما هو ارتفاع صورة الخلفية بالبكسل؟ float height = newSprite.rect.height; //كم عدد البكسلات في الوحدة الواحدة؟ float ppu = newSprite.pixelsPerUnit; //احسب الطول والعرض مستخدما الوحدات width = width / ppu; height = height / ppu; // قم بحساب الموقع الأفقي للصورة الأولى في أقصى اليسار float posX = -width * repeatCount * 0.5f; //قم بحساب حدود المشهد الجديدة Vector2 boundsSize = new Vector2(width * repeatCount, height); Bounds newBounds = new Bounds(Vector2.zero, boundsSize); //قم بإرسال رسالة تخبر بتغير حدود المشهد BroadcastMessage("SceneBoundsChanged", newBounds); //يمكننا الآن البدء ببناء الخلفية الجديدة for(int i = 0; i < repeatCount; i++){ //قم ببناء كائن جديد مستخدما القالب GameObject bg = (GameObject) Instantiate(bgElementPrefab); //قم بتحديد اسم الكائن bg.name = "BG_" + i; //قم بجلب مكوّن تصيير الصور الموجود في الكائن SpriteRenderer sr = bg.GetComponent<SpriteRenderer>(); //قم بتحديد الصورة التي سيتم تصييرها لتكون الصورة المختارة من المصفوفة sr.sprite = newSprite; //قم بضبط الموقع الأفقي مستخدما القيمة التي سبق وحسبناها bg.transform.position = new Vector2(posX, 0); //قم بإرجاع صورة الخلفية خطوة للخلف في ترتيب الرسم sr.sortingOrder = -1; //كأب لعنصر الخلفية الذي تم إنشاؤه "Background" قم بتحديد الكائن الفارغ bg.transform.parent = bgGameObject; //قم بإضافة مكون تصادم على شكل مربع bg.AddComponent<BoxCollider2D>(); //قم بإضافة عرض الصورة إلى قيمة الموقع الأفقي لحساب الموقع الجديد للعنصر التالي posX += width; } //أرسل رسالة تخبر بتغير صورة الخلفية وأرفق معها موقع الصورة الجديدة BroadcastMessage("BackgroundChanged", newIndex); } }
يتعامل محرك Unity مع البريمجات على أنها مكوّنات يمكن إضافتها إلى أي كائن في المشهد أو قالب في المشروع (القوالب في النهاية هي كائنات). بالنسبة لنا فهذا بريمج أساسي يتعلق بإدارة المشهد ككل وليس باللاعب أو الخصم، بالتالي سنضيفه إلى الكائن الجذري وذلك ببساطة عن طريق سحب ملف BackgroundManager.cs من مستعرض المشروع إلى الكائن SceneRoot سواء في الهرمية أو في شاشة خصائصه.
بعد إضافته ستلاحظ أن البريمج ظهر في شاشة خصائص الكائن الجذري وظهرت المتغيرات العامة bgElementPrefab و selectionList و repeatCount بحيث يظهر الأول على شكل خانة فارغة ويظهر الثاني على شكل قائمة يمكنك تحديد عدد عناصرها من الخانة size، والثالث على شكل مربع نص يقبل قيمة رقمية.
لنر الآن كيف يمكننا تحديد قيم هذه المتغيرات وستكون البداية مع bgElementPrefab. كما هو متوقع فهذه الخانة مخصصة لتحديد القالب الذي سيستخدم لاحقا لإنشاء عناصر الخلفية، وهو BGElement الذي سبق وأنشأناه. لربط القالب بهذا المتغير كل ما عليك هو أن تقوم بسحبه من مستعرض المشروع إلى داخل الخانة التي تراها في شاشة الخصائص. بعد ذلك قم بتغيير عدد العناصر في القائمة selectionList إلى 8 ومن ثم اسحب لكل خانة منها واحدة من صور الخلفية الموجودة في المجلد Backgrounds، أما المتغير الثالث repeactCount فقد أخذ قيمة افتراضية هي 4 وهي القيمة التي استخدمناها عند تعريفه. سيصبح شكل الكائن الجذري الآن كالتالي:
لو جربت تشغيل اللعبة الآن ستلاحظ ظهور صورة الخلفية الموجودة في العنصر الأول من القائمة، كما ستلاحظ أنها تكررت أفقيا 4 مرات بشكل صحيح كما ترى أدناه، وتلاحظ أيضا الكائنات الأربعة لهذه الصور والتي تمت إضافتها كأبناء للكائن Background في الهرمية.
سنناقش الآن بشكل سريع أهم آليات عمل البريمج BackgroundManager، جميع البريمجات في محرك Unity تأخذ تقريبا نفس التنسيق، حيث تبدأ بتعريف المتغيرات اللازمة، ومن ثم الدّالتين الأكثر استخداما وهما ()Start و ()Update. كما هو موضح في التعليقات على الكود فإنّ الدّالة ()Start تستدعى مرة واحدة فقط عند بداية التشغيل إن كان الكائن موجودا في المشهد أصلا، أو بمجرد إضافته للمشهد إن لم يكن موجودا من قبل. بعد ذلك تدخل اللعبة فيما نسميها حلقة التحديث أو حلقة تصيير الإطارات حيث يتم استدعاء الدّالة ()Update عند تصيير كل إطار طالما أن اللعبة تعمل. في حالتنا هذه لا يلزمنا استخدام ()Update.
لاحظ أننا بدأنا البريمج بتعريف عدد من المتغيرات بعضها عام public وبعضها الآخر خاص private. بالنسبة لمحرك Unity فإن المتغيرات العامة ذات أهمية خاصة لأنه يعرضها على شكل خانات في شاشة الخصائص مما يسمح لك بتغيير قيمها دون تعديل الكود كما سبق ورأينا. فيما يلي شرح هذه المتغيرات:
- bgElementPrefab يستخدم كمرجع للوصول للقالب الذي سنستخدمه لإنشاء عناصر الخلفية عند بنائها.
- selectionList مصفوفة تحتوي على عناصر من نوع Sprite حيث تقوم بتخزين قائمة بجميع الخلفيات التي يمكن استخدامها.
- repeatCount عدد صحيح يمثل عدد مرات تكرار الخلفية أفقيا، بطبيعة الحال فزيادة هذا العدد سيؤدي لزيادة عرض الخلفية النهائية.
- selectedIndex يمثل موقع الخلفية المعروضة حاليا في القائمة، حيث تمثل القيمة 0 العنصر الأول.
- bgGameObject سنستخدم هذا المتغير للوصول إلى الكائن الفارغ Background والذي خصصناه ليكون أبا لجميع صور الخلفية الموجودة في المشهد. الجدير بالذكر هنا هو أننا في معظم الحالات نستخدم متغيرا من نوع Transform للوصول للكائنات، ولذلك فوائد عديدة سنعرفها.
عند بداية التشغيل يتم استدعاء الدّالة ()Start والتي تقوم أولا بالبحث في الأبناء عن الكائن المسمى Background وتخزينه في المتغير bgGameObject. الخطوة الثانية التي تقوم بها هذه الدّالة هي اختيار العنصر الأول في القائمة تلقائيا ليكون خلفية المشهد وذلك عن طريق استدعاء الدّالة ()ChangeBackground مع القيمة 0.
الدّالة ()ChangeBackground هي الأهم في هذا البريمج حيث تعمل على تغيير الخلفية حسب القيمة التي تعطى لها حين الاستدعاء newIndex. إذا كانت هذه القيمة أصلا تساوي قيمة الخلفية المعروضة حاليا selectedIndex فلا حاجة لتغيير أي شيء بالتالي يتوقف تنفيذ الدّالة. عدا ذلك فإن الدّالة تحتاط لوجود خلفية معروضة حاليا فتقوم بحذفها قبل كل شيء. عملية الحذف تتم عبر المرور على جميع أبناء الكائن bgGameObject واستدعاء الدّالة ()Destroy لها وذلك ليتم حذفها من المشهد. حالة حذف الكائن هي أحد الاستثناءات لقاعدة التعامل مع الكائنات عن طريق المكون Transform، حيث أن الحذف يجب أن يتم على مستوى الكائن كاملا وليس على مكوّن بعينه، بالتالي نستدعي bgSprite.gameObject حين الحذف.
بعد حذف الخلفية السابقة (إن وجدت) نحتاج لإضافة الجديدة. بالتالي فالخطوة التالية ستكون تخزين قيمة الصورة الموجودة في العنصر newIndex في المتغير newSprite ومن ثم حساب عرض الصورة الأصلية بوحدات فضاء Unity وذلك بقسمة عرضها بوحدة البكسل على عدد البكسلات في كل وحدة. بما أن تكرار صورة الخلفية سوف يتمحور حول نقطة الأصل، فإن أقصى نقطة إلى اليسار ستكون نصف عرض جميع الصور التي سنكررها، بالتالي نحسب القيمة posX عن طريق ضرب نصف العدد الكلي للصور repeatCount بعرض الصورة الواحدة width، ونحول الناتج للقيمة السالبة حتى نبدأ من اليسار (تذكر أننا قمنا باختيار يسار الصورة ليكون نقطة ارتكازها في الفضاء).
إضافة لنقطة البداية قمنا بحساب الطول والعرض الكلي للمشهد بعد بناء الخلفية الجديدة، ومن ثم قمنا بتخزين هذه القيم في متغير من نوع Bounds، هذا المتغير يخزن ببساطة نقطة في الفضاء وامتدادات لهذه النقطة على المحورين x و y. لاحظ أننا عرّفنا متغير الامتداد على شكل متجه ثنائي الأبعاد وحددنا قيمة x بأنها عدد مرات تكرار صورة الخلفية مضروبة في عرض الصورة الواحدة و قيمة y على أنها ارتفاع الصورة الواحدة. بعد ذلك قمنا بإرسال رسالة لجميع البريمجات الأخرى سواء الموجودة على الكائن الجذري أو أي كائن في الأبناء وذلك لإعلامها بأن حدود المشهد تغيرت، واستخدمنا لذلك الدّالة ()BroadcastMessage. عند استدعاء هذه الدّالة فإننا نعطيها اسم الرسالة وهو هنا SceneBoundsChanged ونرفق معها أيضا المتغير newBounds الذي يحتوي على حدود المشهد الجديدة. سنرى لاحقا ما معنى أن ترسل رسالة، وكيف تستقبلها، ولماذا هي مهمة. خلال مجموعة الدروس هذه سنتعامل بشكل مستفيض مع الرسائل، لذا لا بأس من التعرف مبكرا على كيفية إرسالها. يتم إرسال الرسائل في محرك Unity باستخدام أحد الدّوال التالية:
- BroadcastMessage والتي تقوم بإرسال رسالة لجميع البريمجات الموجودة على الكائن الحالي إضافة إلى جميع كائنات الأبناء والأحفاد المتفرعة منه.
- SendMessage والتي تقوم بإرسال الرسالة إلى جميع البريمجات الموجودة على الكائن الحالي فقط.
- SendMessageUpwards والتي تقوم بإرسال الرسالة إلى جميع بريمجات الكائن الحالي إضافة إلى جميع آبائه وأجداده في الهرمية.
الحلقة التكرارية التي تلي هذه الخطوات هي التي يتم عبرها بناء وتوزيع صور الخلفية في الفضاء بالشكل الصحيح. حيث نبدأ بإنشاء كائن جديد bg مستخدمين القالب bgElementPrefab ثم نعطيه اسما متسلسلا حسب موقعه في مجموعة الخلفيات من اليسار لليمين (مثلا BG_0 سيكون اسم الخلفية في أقصى اليسار و BG_3 في أقصى اليمين). بعد ذلك نستدعي المكوّن Sprite Renderer ونحدد له الصورة التي سيقوم بتصييرها، وهي هنا newSprite التي سبق واستخرجناها من المصفوفة selectionList. مرة أخرى تعاملنا مع الكائن مباشرة وليس مع مكون Transform، وذلك لأن الهدف هنا هو إضافة مكون جديد له.
أما الخطوتان التاليتان فهما تتعاملان مع bg.transform كما ترى، فالأولى تحدد موقعه في الفضاء gb.transform.position والثانية تحدد الكائن الأب له وهو هنا bgGameObject الذي سبق وتحدثنا عنه. بالنسبة للموقع فإن الخلفية تتوسط الشاشة عموديا كما ترى حيث y=0، أما أفقيا فالموقع هو القيمة posX التي سبق وحسبناها. من المهم هنا إدراك حقيقة أن صور الخلفية يجب أن تظهر خلف باقي الصور الموجودة في فضاء اللعبة. من أجل ذلك علينا أن نخبر مكوّن التصيير أن يقوم برسم الخلفية قبل أن يرسم أي صورة أخرى وذلك حتى لا تغطي الخلفية على عناصر المشهد. هذا الأمر يمكننا القيام به عن طريق تقليل قيمة المتغير sr.sortingOrder حيث القيمة الافتراضية هي صفر. الصور ذات القيمة الأقل للمتغير sortingOrder يتم تصييرها أولا تتبعها الصور ذات القيمة الأعلى فالأعلى.
بعد ذلك نضيف مكوّن اكتشاف التصادمات Box Collider 2D حتى نتمكن من اكتشاف قيام اللاعب بلمس الخلفية أو النقر عليها بالفأرة. الخطوة الأخيرة هي زيادة posX بمقدار عرض الصورة width حتى يكون موقع الصورة التالية على يمين الحالية مباشرة وملاصقة لطرفها الأيمن. بعد بناء الخلفية نرسل رسالة أخرى هي BackgroundChanged ونرفق معها رقم عنصر الخلفية الجديدة، والتي سنرى لاحقا أهميتها.
بناء نظام التحكم بالكاميرا
لو قمت بتشغيل اللعبة ستلاحظ أن الكاميرا تنظر مباشرة لوسط المشهد، وما سنقوم به الآن هو إضافة آلية لتحريكها يمينا ويسارا، إضافة إلى إمكانية التقريب والإبعاد. قبل البدء في تفاصيل التحكم بالكاميرا علينا أن نحدد ما هي نسبة عرض الشاشة إلى طولها aspect ratio. هذه القيمة مهمة لأنها تؤثر مباشرة على حجم المستطيل الذي يحدد مجال رؤية الكاميرا. بما أن هذه اللعبة مصممة للهواتف وتلعب بشكل عرضي، فإن النسبة التي سنعتمدها هي 16:9، ولاختيارها انتقل من شاشة المشهد Scene إلى شاشة اللعبة Game وقم باختيار هذه النسبة من أعلى يسار الشاشة كما ترى هنا:
قبل البدء في كتابة بريمج التحكم بالكاميرا، علينا أن نعرف طبيعة هذا التحكم وماذا يمكن للاعب أن يفعل من خلاله. طريقة التحكم ستكون ثنائية البعد، أي أن اللاعب سيتمكن من نحريك الكاميرا يمينا ويسارا ولأعلى وأسفل. إضافة لذلك فإن اللاعب يجب أن يكون قادرا على تقريب وإبعاد الكاميرا، وكل ذلك ضمن حدود معينة. سنطّلع بعد قليل على بريمج التحكم بالكاميرا ومن ثم نشرح بعض تفاصيله، وأخيرا سنرى كيف يمكننا ربط وظائف هذا البريمج مع مدخلات اللاعب.
لكن قبل ذلك لنتعلم أحد المبادئ المهمة في برمجة الألعاب وخاصة مع محرك Unity وهي فصل الاهتمامات separation of concerns. معنى هذا أن البريمج الواحد مسؤول عن مهمة واحدة، وهو الوحيد المخول بالتحكم بتفاصيل هذه المهمة. بالتالي فإننا سنقوم أولا بكتابة بريمج للتحكم بالكاميرا، لكنه منفصل تماما عن مدخلات اللاعب. هذه المدخلات سنكتبها في بريمج آخر خاص بها، والذي سيستقبلها وبناء عليها "يطلب" من بريمج التحكم بالكاميرا تحريكها. هذا الطلب قد يستجاب له وقد لا يستجاب له وفقا لمعطيات يقدرها بريمج التحكم بالكاميرا. مثلا إذا حرك اللاعب الكاميرا لأقصى يمين المشهد ومن ثم حاول تحريكها لليمين متجاوزا حدود المشهد، فسيقوم بريمج الكاميرا برفض هذا الطلب وستبقى الكاميرا في مكانها.
لنبدأ الآن مع البريمج CameraControl والذي تراه في السرد التالي، هذا البريمج يجب أن تتم إضافته إلى كائن الكاميرا حتى يقوم بالتحكم بها:
using UnityEngine; using System.Collections; public class CameraControl : MonoBehaviour { //أقل حجم للكاميرا في حال التقريب public float minSize = 2.5f; //ارتفاع المستطيل المحدد لمجال الرؤية private float camHeight; //عرض المستطيل المحدد لمجال الرؤية private float camWidth; //مرجع إلى مكوّن الكاميرا Camera cam; //متغير لتخزين حدود المشهد الحالية private Bounds sceneBounds; //يتم تنفيذها مرة عند بداية التشغيل void Start () { //اجلب مكوّن الكاميرا الموجود على نفس الكائن cam = GetComponent<Camera>(); //قم بتحديث أبعاد الكاميرا UpdateCamDimensions(); } //يتم استدعاؤها مرة عند تصيير كل إطار void Update () { } //قم باستقبال الرسالة التي تخبر بتغير حدود المشهد void SceneBoundsChanged(Bounds newBounds){ sceneBounds = newBounds; } //تقوم هذه الدّالة بتحريك الكاميرا بمقدار محدد public void Move(Vector2 distance){ //قم بحساب حدود الحركة المسموحة float maxX = GetRightLimit(); float minX = GetLeftLimit(); float maxY = GetUpperLimit(); float minY = GetLowerLimit(); //يهمنا في هذه الحالة الخاصّة الموقع ثلاثي الأبعاد //z = -10 وذلك حتى نضمن أن الكاميرا في الموقع //دائما Vector3 dist3D = distance; //احسب الموقع الجديد Vector3 newPos = transform.position + dist3D; //التزم بحدود الحركة newPos.x = Mathf.Clamp(newPos.x, minX, maxX); newPos.y = Mathf.Clamp(newPos.y, minY, maxY); newPos.z = -10.0f; //قم بتغيير الموقع transform.position = newPos; } //تقوم هذه الدّالة بتغيير تقريب الكاميرا //القيمة الموجبة تعني تقريب الكاميرا من المشهد //القيمة السالبة تعني إبعاد الكاميرا عن المشهد public void Zoom(float amount){ float newSize = cam.orthographicSize - amount; //التزم بحدود التقريب والإبعاد float maxSize = GetMaxSize(); newSize = Mathf.Clamp (newSize, minSize, maxSize); //قم بتغيير حجم مجال الرؤية وبالتالي يتغير التقريب cam.orthographicSize = newSize; //قم بتحريك الكاميرا بمقدار صفر حتى تضمن تنفيذ الالتزام بحدود المشهد UpdateCamDimensions(); Move(Vector2.zero); } //تقوم بتحديث عرض وارتفاع الكاميرا void UpdateCamDimensions(){ camHeight = cam.orthographicSize * 2; camWidth = camHeight * cam.aspect; } //تحسب قيمة أعلى ارتفاع للكاميرا float GetUpperLimit(){ return sceneBounds.max.y - camHeight * 0.5f; } //تحسب قيمة أدنى ارتفاع للكاميرا float GetLowerLimit(){ return sceneBounds.min.y + camHeight * 0.5f; } //تحسب الحد الأقصى لموقع الكاميرا يمينا float GetRightLimit(){ return sceneBounds.max.x - camWidth * 0.5f; } //تحسب الحد الأقصى لموقع الكاميرا يسارا float GetLeftLimit(){ return sceneBounds.min.x + camWidth * 0.5f; } //تقوم بحساب أكبر حجم مسموح للكاميرا حين الإبعاد //وذلك اعتمادا على حدود المشهد float GetMaxSize(){ float maxHeight = sceneBounds.max.y - sceneBounds.min.y; return maxHeight * 0.5f; } }
المتغير العام الوحيد في هذا البريمج هو minSize والذي يحدد أقل حجم مسموح للكاميرا عند التقريب. لشرح آلية التقريب والإبعاد لاحظ أولا أن الكاميرا لا ترى كل المشهد في كل وقت ولكن لها مجال رؤية محدد بشكل مستطيل، ويظهر هذا المستطيل حين اختيار الكاميرا كما في الشكل التالي:
كلما زاد حجم هذا المستطيل فإن المساحة التي يراها من المشهد تزيد، بالتالي يبدو الأمر وكأن الكاميرا تبتعد، أما في حالة تقليل حجمه فإن العكس يحصل وهو تقليل المساحة المرئية بحيث يبدو الأمر كأن الكاميرا تقترب. يمكنك تجربة ذلك أثناء تشغيل اللعبة عن طريق تعديل قيمة المتغير size في مكوّن الكاميرا من شاشة الخصائص حيث قيمته الافتراضية هي 5.
معنى هذا أننا نسمح بتقريب الكاميرا أي تصغير حجمها إلى أن يصل إلى 2.5 ويمكنك السماح بتقريبها أكثر إن قمت بتقليل هذا الرقم. المتغيران الآخران المهمان هما camHeight و camWidth ويمثلان طول وعرض المستطيل الذي يحدد مجال الرؤية، لكنهما محسوبان بقيم فضاء Unity وسنرى أهميتهما بعد قليل. أيضا نحتاج لمرجع لمكوّن الكاميرا وهو cam. تذكر ما ذكرته سابقا وهو أن المكوّنات هي التي تميز كائنا عن الآخر، ووجود هذا المكوّن هو الذي يجعل الكائن المسمى Main Camera عبارة عن كاميرا وليس شيئا آخر. أخيرا لدينا متغير من نوع Bounds وهو sceneBounds والذي سيقوم بتخزين حدود المشهد والتي سنحصر حركة الكاميرا دخلها.
سأبدأ هذه المرة مع الدّالة ()SceneBoundsChanged وذلك لإكمال شرح فكرة الرسائل. بالعودة إلى البريمج BackgroundManager سترى أنه يقوم بإرسال رسالة اسمها SceneBoundsChanged ويرفقها بمتغير من نوع Bounds. إذا قمت بتعريف دالّة تحمل نفس اسم الرسالة وتأخذ متغيرا من نفس نوع المرفق، فإنك بذلك حددت هذه الدّالة كأحد مستقبلي هذه الرسالة (يمكن أن يستقبل الرسالة عدد غير محدد من البريمجات). بالتالي فالبريمج CameraControl يستقبل هذه الرسالة ويخزن قيمة الحدود الجديدة للمشهد في المتغير sceneBounds.
بالعودة إلى ()Start فإن ما تقوم به هو تخزين قيمة مكوّن الكاميرا في المتغير cam ومن ثم تقوم باستدعاء ()UpdateCamDimensions والتي تحسب عرض وارتفاع الكاميرا بوحدات فضاء Unity. لنتحدث قليلا عن كيفية حساب هذه القيم. بالرجوع إلى توثيق محرك Unity وتحديدا هذه الصفحة سنعرف أن قيمة المتغير cam.orthographicSize والذي يمثل حجم الكاميرا هو عبارة عن منتصف ارتفاعها. بالتالي يمكننا ضرب هذه القيمة في 2 لنحصل على ارتفاع الكاميرا. بالنسبة لعرضها فهو يعتمد على نسبة العرض إلى الارتفاع aspect ratio، والذي يمكننا الوصول إليه عبر المتغير cam.aspect، حيث يمكننا ضرب هذا المتغير في الارتفاع لنحصل على عرض الكاميرا.
بحصولنا على حدود المشهد وارتفاع الكاميرا وعرضها، يمكننا أن نحسب الحدود التي يُسمح للكاميرا بالحركة ضمنها. سيُسمح للكاميرا بالحركة يمينا إلى أن يصل طرف مستطيل مجال الرؤية الأيمن إلى أقصى حد في يمين المشهد. بالتالي فإن أقصى موقع للكاميرا يمينا هو طرف المشهد الأيمن مطروح منه نصف عرض الكاميرا. هذه القيمة نحسبها عن طريق الدّالة ()GetRightLimit، وبطريقة مشابهة نحسب الحدود الأخرى في اليسار والأعلى والأسفل عبر ()GetLeftLimit و ()GetUpperLimit و ()GetLowerLimit. لاحظ أننا في هذه الدّوال استخدمنا sceneBounds.min و sceneBounds.max وهما المتجهان اللذان يعبران عن الحد الأدنى (أسفل ويسارا) والحد الأقصى (أعلى ويمينا) لحدود المشهد.
قبل الدخول في الوظائف الرئيسية للبريمج وهما التحريك والتقريب، لنتعرف على الدّالة المتبقية ضمن الدّوال المساعدة وهي ()GetMaxSize. تعمل هذه الدّالة على حساب الحد الأقصى المسموح لحجم الكاميرا حين تنفيذ الإبعاد، وذلك اعتمادا على الحدين العلوي والسفلي للمشهد إضافة لارتفاع الكاميرا. فالارتفاع الأقصى المسموح به هو المسافة بين حد المشهد الأعلى والأسفل، والحجم المسموح به بالتالي هو نصف هذه المسافة، ذلك لأن حجم الكاميرا هو نصف ارتفاعها كما سبق وعرفنا.
نأتي الآن على الوظائف الأساسية لهذا البريمج وهي تحريك الكاميرا وتقريبها وإبعادها. البداية مع التحريك والذي يتم عن طريق الدّالة ()Move والتي تأخذ متجها يمثل مقدار الإزاحة التي نرغب بتحريك الكاميرا بها. أول خطوة هي حساب القيم القصوى والدنيا للحركة المسموح بها، وتخزينها في المتغيرات الأربع maxX, minX, maxY, minY. بعد ذلك نقوم بتعريف متجه ثلاثي الأبعاد هو dist3D وذلك لأن موقع الكاميرا على محور الثالث z مهم بخلاف الكائنات الأخرى، حيث يجب أن تبقى هذه القيمة سالبة أي أن الكاميرا بعيدة عن المشهد نحو الخارج وتنظر إليه داخل الشاشة. أما إذا حسبنا موقعها مستخدمين الأبعاد الثنائية فقط فإن هذا سيؤدي لضياع القيمة z للموقع وتصبح الكاميرا في الموقع z=0 وبالتالي لن يمكنها رؤية أي كائن في المشهد.
بعد حساب الموقع الجديد نقوم بتخزينه في الموقع newPosition وكل ما علينا الآن هو التأكد من أن قيمة newPos.x محصورة بين minX و maxX وكذلك الأمر بالنسبة لقيمة newPos.y التي يجب أن تنحصر بين minY و maxY. لاحظ هنا استخدام الدّالة ()Mathf.Clamp والتي تقوم بالتأكد من أن الرقم الأول محصور بين الرقمين الثاني والثالث وتعيد الحد الأقصى أو الأدنى في حال كان الرقم يتجاوز هذين الحدين. أخيرا نقوم بتغيير موقع الكاميرا للموقع الجديد newPos.
الوظيفة الرئيسية الثانية هي تقريب الكاميرا وإبعادها، والتي تتم عن طريق الدّالة ()Zoom، هذه الدّالة تأخذ رقما واحدا وهو مقدار التقريب (إن كان موجبا) أو الإبعاد (إن كان سالبا). طريقة التقريب والإبعاد تتم عن طريق طرح القيمة amount من حجم الكاميرا الحالي، وذلك لأن العلاقة بين حجم الكاميرا وتقريبها عكسية، فزيادة الحجم تعني تقليل التقريب. بعدها يتم حصر القيمة الجديدة بين minSize وهو المتغير الذي يمكننا تحديده كما نشاء و maxSize والذي يتم حسابه عن طريق الدّالة ()GetMaxSize التي سبق شرحها. بعد التأكد من أن الحجم الجديد ضمن الحدود يتم تغيير قيمة cam.orthogonalSize للقيمة الجديدة newSize. أخيرا يجب أن نعيد حساب أبعاد الكاميرا حيث أن حجمها تغير لذا نستدعي ()UpdateCamDimensions إضافة إلى إزاحتها بمقدار صفر احتياطا من إمكانية كون تغيير حجمها قد أدى لتجاوز حدودها لحدود المشهد خاصة في حالة الإبعاد حيث يزيد حجم الكاميرا؛ حيث أن الإزاحة بغض النظر عن مقدارها ستُبقي الكاميرا ضمن حدود المشهد دائما.
بهذا تكون آلية التحكم بالكاميرا جاهزة، ويبقى علينا أن نربط استدعاء كل من ()Move و ()Zoom بمدخلات اللاعب. البداية ستكون مع الفأرة وذلك لأن المحرك يعمل على الحاسب بالتالي من السهل تجربتها مباشرة، بينما سنؤجل شاشة اللمس والهواتف الذكية لوقت لاحق. تحريك الكاميرا سيتم عن طريق الضغط بزر الفأرة الأيسر على الخلفية ومن ثم تحريكها في الاتجاهات الأربع، بينما يستخدم الزر الأيمن للتقريب والإبعاد، حيث يضغط اللاعب بالزر الأيمن ويحرك الفأرة لليمين للتقريب ولليسار للإبعاد. هذه الوظائف سيقوم بها البريمج CameraMouseInput والذي يجب أن نضيفه لقالب الخلفية BGElement، حيث يجب أن تكون كل صور الخلفية قادرة على استقبال مدخلات الفأرة. فيما يلي سرد بهذا البريمج.
using UnityEngine; using System.Collections; public class CameraMouseInput : MonoBehaviour { //سرعة حركة الكاميرا public float movementSpeed = 0.1f; //سرعة التقريب والإبعاد public float zoomingSpeed = 0.01f; //أثناء التقريب والإبعاد trueتكون قيمة هذا المتغير private bool zoomingAcitve = false; //تقوم بتخزين موقع مؤشر الفأرة في الإطار السابق private Vector2 lastPosition; //مرجع لبريمج التحكم بالكاميرا private CameraControl camControl; //يتم تنفيذ هذه الدّالة مرة عند بداية التشغيل void Start () { //اجلب بريمج التحكم بالكاميرا camControl = FindObjectOfType<CameraControl>(); } //يتم استدعاؤها عند تصيير كل إطار لكن في وقت متأخر void LateUpdate () { UpdateZooming(); } //تقوم بقراءة مدخلات التقريب والإبعاد void UpdateZooming(){ //قم بفحص الضغط على زر الفأرة الأيمن if(Input.GetMouseButtonDown(1)){ zoomingAcitve = true; lastPosition = Input.mousePosition; } //قم بتحديث التقريب والإبعاد بناء على الحركة الأفقية للفأرة if(zoomingAcitve){ float amount = Input.mousePosition.x - lastPosition.x; camControl.Zoom(amount * zoomingSpeed); lastPosition = Input.mousePosition; } //عند رفع الضغط عن زر الفأرة الأيمن false إلى zoomingActive قم بإرجاع قيمة if(Input.GetMouseButtonUp(1)){ zoomingAcitve = false; } } //يتم استدعاء هذه الدّالة عند الضغط بزر الفأرة الأيمن على الكائن void OnMouseDown(){ //لا تسمح بالحركة أثناء التقريب والإبعاد if(!zoomingAcitve){ lastPosition = Input.mousePosition; } } //يتم استدعاء هذه الدّالة عند السحب بزر الفأرة الأيسر فوق الكائن void OnMouseDrag(){ //امنع الحركة أثناء التقريب والإبعاد if(!zoomingAcitve){ Vector2 movement = lastPosition - (Vector2)Input.mousePosition; camControl.Move(movement * movementSpeed); lastPosition = Input.mousePosition; } } }
كما ذكرت سابقا يجب أن تتم إضافة هذا البريمج على قالب صور الخلفية، وذلك لأن اللاعب سيضغط عليها بالفأرة إذا أراد تحريك الكاميرا. عند بداية تشغيل البريمج فإنه يبحث في المشهد عن البريمج الذي يتحكم بالكاميرا CameraController وذلك عن طريق ()FindObjectOfType، وبما أن هذا البريمج موجود مرة واحدة في المشهد بالتالي ستكون نتيجة البحث هي قطعا البريمج الموجود على الكاميرا. بالتالي يمكننا الآن استخدام المتغير camController للتحكم بالكاميرا.
عندما يضغط اللاعب بزر الفأرة الأيسر على كائن ما يتم إرسال الرسالة OnMouseDown مرة واحدة إلى ذلك الكائن، لذا فإننا هنا نستقبل هذه الرسالة عبر الدّالة التي تحمل نفس الاسم حيث نقوم بتخزين موقع الفأرة الحالي في المتغير lastPosition. إذا بقي اللاعب ضاغطا على الزر الأيسر وقام بتحريك الفأرة، فحينها يتم إرسال الرسالة OnMouseDrag إلى الكائن في كل إطار يتم فيه تحريك المؤشر أثناء الضغط. في الدّالة ()OnMouseDrag نقوم بثلاثة أشياء:
- أولا نحسب مقدار الإزاحة عن موقع الضغط وذلك بطرح الموقع الحالي من الموقع السابق. بهذه الطريقة فإننا نجعل حركة الكاميرا معاكسة لحركة المؤشر، فإن تحرك المؤشر يمينا تتحرك الكاميرا يسارا، مما يعطي اللاعب شعورا بأنه لا يحرك الكاميرا، وإنما يمسك المشهد ويحركه يمينا ويسارا وهذه الطريقة أسهل عليه في التحكم خاصة مع شاشة اللمس.
- الخطوة الثانية هي استدعاء الدّالة ()Move وتزويدها بمقدار الإزاحة المحسوبة مضروبة في سرعة الحركة. لاحظ أن سرعة الحركة قليلة نسبيا وذلك لأن قيمة إزاحة الفأرة عالية مقارنة مع مقدار الإزاحة المطلوبة للكاميرا.
- أخيرا وبعد تنفيذ الإزاحة نقوم بتخزين موقع المؤشر الحالي في lastPosition حتى نكون جاهزين لحساب الإزاحة القادمة انطلاقا من الموقع الجديد.
لاحظ أن جميع الخطوات في الدّالتين ()OnMouseDown و ()OnMouseDrag مشروطتان بأن تكون قيمة zoomingActive هي false. هذا المتغير يخبرنا ما إذا كان اللاعب يقوم حاليا بتقريب أو إبعاد الكاميرا مستخدما الزر الأيمن، وبذلك نمنع التحريك والتقريب في آن واحد. الدّالة ()LateUpdate التي عرّفناها هنا بدلا من ()Update تختلف عن هذه الأخيرة في شيء واحد، وهو أنها تُستدعى متأخرة عنها. فعند تحديث كل إطار يقوم Unity بتحديث جميع البريمجات عن طريق استدعاء ()Update ومن ثم يعاود التحديث مجددا باستخدام ()LateUpdate. بالتالي فإن استدعاء أي خطوات خلال ()LateUpdate هو مضمون أن يتم تنفيذه بعد تحديث جميع عناصر المشهد من خلال ()Update. الفائدة من ذلك هو أن نضمن أن كل العناصر أصبحت جاهزة للإطار الجديد قبل أن نقوم بتحريك الكاميرا، بالتالي عادة ما يتم استخدام ()LateUpdate مع أي شيء له علاقة بالتحكم بالكاميرا.
ما نفعله في ()LateUpdate هو استدعاء ()UpdateZooming والتي تفحص ما إذا ضغط اللاعب على زر الفأرة الأيمن عن طريق الدّالة ()Input.GetMouseButtonDown والتي تفحص أزرار الفأرة بناء على أرقامها؛ فالرقم 0 للأيسر و 1 للأيمن و 2 للأوسط. أول ما نقوم به عند اكتشاف الضغط على الزر الأيمن هو تغيير قيمة zoomingActive إلى true مما يمنع استقبال مدخلات التحريك. بعدها نقوم بتخزين الموقع الحالي للمؤشر في lastPosition تماما كما فعلنا مع حالة الضغط بالزر الأيسر. بناء على قيمة zoomingActive نقوم بتحديث التقريب والإبعاد، فنحسب قيمة الإزاحة الأفقية بطرح الإحداثي x للموقع السابق من الإحداثي x للموقع الحالي، فإذا تحرك مؤشر الفأرة يمينا ستكون النتيجة موجبة مما يجعل قيمة amount * zoomingSpeed المزودة للدّالة ()camControl.Zoom موجبة وينتج عنه تقريب الكاميرا، والعكس يحدث حين تحريك الفأرة يسارا. بعد تحديث قيمة التقريب نقوم بتخزين الموقع الحالي في lastPosition من أجل حساب الإزاحة القادمة. آخر خطوة في هذه الدّالة هي فحص ما إذا كان اللاعب قد رفع الضغط عن زر الفأرة الأيمن وإعادة قيمة zoomingSpeed إلى false في هذه الحالة.
بناء أرضية المشهد
بعد أن انتهينا من تجهيز خلفية المشهد وأصبحنا قادرين على تحريك الكاميرا للتجول فيه بحرية، حان الوقت لإضافة عناصر المشهد، وهي الكائنات ثنائية الأبعاد التي سنبني منها اللعبة. ستكون البداية مع الأرضية التي ستقف عليها باقي الكائنات، والتي سنبنيها بطريقة مشابهة لطريقة بناء الخلفية. سنقوم أولا بإضافة كائن فارغ كابن للكائن الجذري ونسميه Ground. هذا الكائن سيكون أبا لجميع كائنات الأرضية. بعدها سنقوم بإنشاء قالب خاص بكائنات الأرضية ونضيف له المكوّن sprite renderer والبريمج CameraMouseInput، مما يجعل التحكم بالكاميرا عن طريق الضغط على الأرضية ممكنا تماما كما هو الحال مع الخلفية. سنسمي هذا القالب GroundElement ومن ثم سنكتب البريمج الذي يعمل على إضافة كائنات الأرضية اعتمادا على الخلفية وحدود المشهد. هذا البريمج هو GroundManager والموضح في السرد التالي، علما أن هذا البريمج يجب أن نضيفه أيضا للكائن الجذري SceneRoot.
using UnityEngine; using System.Collections; public class GroundManager : MonoBehaviour { //قالب كائن عناصر الأرضية public GameObject gePrefab; //مصفوفة بصور الأرضية المتوفرة public Sprite[] selectionList; //متغير لتخزين حدود المشهد الحالية private Bounds sceneBounds; //موقع صورة الخلفية المعروضة حاليا private float selectedIndex = -1; //تستدعى مرة واحدة عند بداية التشغيل void Start () { } //تستدعى مرة واحدة عند تصيير كل إطار void Update () { } //تقوم باستقبال رسالة تغير صورة الخلفية void BackgroundChanged(int newIndex){ ChangeGround(newIndex); } //تقوم باستقبال رسالة تغير حدود المشهد void SceneBoundsChanged(Bounds newBounds){ sceneBounds = newBounds; } //تقوم بتغيير صورة الأرضية المعروضة بناء على رقم العنصر المحدد public void ChangeGround(int newIndex){ //"Ground" ابحث عن الكائن الابن المسمى Transform groundGameObject = transform.FindChild("Ground"); //قم بحذف عناصر الأرضية الحالية for(int i = 0; i < groundGameObject.childCount; i++){ Transform ge = groundGameObject.GetChild(i); Destroy(ge.gameObject); } //قم بحساب عرض المشهد float sceneWidth = sceneBounds.max.x - sceneBounds.min.x; //قم بحساب النقطة في أقصى اليسار float posX = sceneBounds.min.x; //قم بجلب صورة الأرضية من المصفوفة Sprite newGround = selectionList[newIndex]; //قم بحساب عرض وارتفاع صورة الأرضية float width = newGround.rect.width; float height = newGround.rect.height; //قم بحساب العرض والارتفاع بالوحدات width = width / newGround.pixelsPerUnit; height = height / newGround.pixelsPerUnit; //قم بحساب الموقع العمودي للأرضية float posY = sceneBounds.min.y + height * 0.5f; //كم مرة يجب أن نكرر صورة الأرضية؟ int repeats = Mathf.RoundToInt(sceneWidth / width) + 1; for(int i = 0; i < repeats; i++){ //قم بإضافة كائن عنصر أرضية جديد GameObject ge = (GameObject) Instantiate(gePrefab); ge.name = "GE_" + i; //ضع الكائن في الموقع الصحيح ge.transform.position = new Vector2(posX, posY); //قم بتحديد الأب ge.transform.parent = groundGameObject; //قم بوضع الصورة على مكوّن تصيير الكائن الذي أضفناه SpriteRenderer sr = ge.GetComponent<SpriteRenderer>(); sr.sprite = newGround; //قم بإضافة مكوّن التصادم ge.AddComponent<BoxCollider2D>(); //قم بزيادة قيمة الموقع الأفقي استعدادا للعنصر التالي posX += width; } } }
ما نلاحظه للوهلة الأولى هو التشابه الشديد بين GroundManager و BackgroundManager حثي أن كلا منهما يقوم ببناء عدة كائنات مستخدما قالبا محددا ومن ثم رص هذه الكائنات بجانب بعضها البعض من اليسار لليمين. الفرق هنا هو أن هذا البريمج يعتمد في عمله على استقبال الرسالة BackgroundChanged بحيث يختار صورة الأرضية الموجودة في الموقع المساوي لموقع الخلفية التي تم اختيارها. معنى ذلك أنه لصور الخلفية الثمانية الموجودة سنحتاج لصور ثماني أرضيات. صور الأرضيات موجودة في المجلد Other في مجموعة الصور وعددها هو 4 كما ترى في الصورة التالية، مما يعني أن نفس الأرضية يمكن أن تتكرر مع أكثر من خلفية:
أول خطوة علينا القيام بها هي تغيير نقطة الارتكاز لهذه الصور الأربعة بحيث تصبح في اليسار تماما كما فعلنا مع صور الخلفية. ومن ثم سنقوم بسحبها إلى المواقع الثمانية داخل المصفوفة selectionList. أثناء إضافتي لصورة الأرضية المقابلة لكل خلفية، حاولت أن أختار لونا متميزا عن لون الجزء الأسفل من الخلفية، بحيث يسهل على اللاعب تمييز أرضية المشهد عن خلفيته، وكانت النتيجة كالتالي. لا تنس أيضا بعد إضافة البريمج للكائن SceneRoot أن تقوم بتحديد القالب GroundElement ليكون هو مصدر بناء كائنات الأرضية.
اختلاف آخر بين البريمجين نلحظه في موقع وضع الكائنات، حيث توضع كائنات الخلفية عموديا في المنتصف بينما توضع كائنات الأرضية في الأسفل. الفرق الآخر المهم هو أن عدد مرات التكرار بالنسبة للخلفية متغير يمكننا تحديد قيمته، بينما في حالة الأرضية يتم حسابه تلقائيا بقسمة عرض المشهد على عرض صورة الأرضية، ومن ثم إضافة 1 لتغطية أي ثغرة متبقية في أقصى اليمين نتيجة للكسور.
إلى هذه النقطة نكون قد انتهينا من بناء الخلفية والأرضية، بالتالي لو حاولنا تشغيل اللعبة الآن سنحصل على النتيجة التالية:
قمنا في هذا الدرس بتجهيز الخلفية و الأرضية للعبة كما قمنا بالتحكم في الكاميرا. في الدرس القادم سنقوم بإنشاء الوحدات البنائية للعبة.
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.