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

البحث في الموقع

المحتوى عن 'unity3d'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 3 نتائج

  1. نبذة مختصرة عن صناعة الألعاب وتطويرها لعل صناعة الألعاب هي إحدى أصعب الصناعات في هذا العصر، وذلك من عدة نواح تبدأ بالتحديات التقنية، مرورا بجمهور صعب الإرضاء ولا يرحم حتى كبريات الشركات إن لم تكن منتجاتها بالمستوى المطلوب، وليس انتهاءا بالمنافسة الشرسة ونسب الفشل العالية وصعوبة تحقيق أرباح تغطي تكاليف الإنتاج المرتفعة. على الجانب الآخر يوجد ميزات لهذه الصناعة تجعل من النجاة فيها أمرا ممكنا، فعلى الجانب التقني مثلا، لا تخلوا الغالبية العظمى من الألعاب من وظائف متشابهة وأنماط متكررة من معالجة البيانات، مما يجعل إعادة استخدام الوحدات البرمجية للألعاب السابقة من أجل إنشاء ألعاب جديدة أمرا ممكنا، وهذا بدوره يساهم في تذليل العقبات التقنية واختصار الوقت والجهد. عندما تتحدث عن صناعة لعبة، فأنت هنا تذكر العملية الكبرى والتي تنطوي على عشرات وربما مئات المهام التي يجب أن تنجزها في مجالات عدة. فصناعة لعبة تعني إنتاجها وتسويقها ونشرها وكل ما يتعلق بهذه العمليات من إجراءات وخطوات إدارية وتقنية وفنية ومالية وقانونية. على أية حال فإن ما يهمنا في سلسلة الدروس هذه هو الجانب التقني وهو تطوير اللعبة، وهي عملية بناء المنتج البرمجي النهائي بكافة مكوّناته. هذه العملية لا تشمل بالضرورة تصميم اللعبة، حيث أن عملية التصميم ذات منظور أوسع وتركز على أمور مثل القصة والسمة العامة للعبة وأشكال المراحل وطبيعة الخصوم، بالإضافة إلى قواعد اللعبة وأهدافها وشروط الفوز والخسارة. بالعودة لعملية تطوير اللعبة، نجد أن العديد من الاختصاصات والمهارات تساهم في هذه العملية، فهناك الرسامون ومصممو النماذج وفنيو التحريك ومهندسو الصوت والمخرج، إضافة – بالطبع – للمبرمجين. هذه النظرة الشاملة مهمة لنعرف أن دور المبرمج في إنتاج اللعبة ليس سوى دورا مكمّلا لأدوار غيره من أعضاء الفريق، ولو أن هذه الصورة بدأت تتغير بظهور المطورين المستقلين Indie Developers والذين يقومون بالعديد من المهام إلى جانب البرمجة. لماذا نستخدم محركات الألعاب؟ لو أردنا الحديث بتفصيل أكبر عن دور المبرمجين في صناعة الألعاب، سنجد أنه حتى على مستوى البرمجة نفسها هناك أدوار عديدة يجب القيام بها: فهناك برمجة الرسومات وهناك أنظم الإدخال وأنظمة استيراد الموارد والذكاء الاصطناعي ومحاكاة الفيزياء وغيرها مثل مكتبات الصوت والأدوات المساعدة. كل هذه المهام يمكن إنجازها على شكل وحدات برمجية قابلة لإعادة الاستخدام كما سبق وذكرت، وبالتالي فهذه الوحدات تشكل معا ما يعرف بمحرك الألعاب Game Engine. باستخدامك للمحرك والمكتبات البرمجية التي يتكون منها، فأنت تختصر على نفسك الجهد اللازم لبناء نظام الإدخال والإخراج والاستيراد ومحاكاة الفيزياء، وحتى جزء من الذكاء الاصطناعي. وما يتبقى عليك هو كتابة منطق لعبتك الخاصة وإبداع ما يميزها عن غيرها من الألعاب. هذه النقطة الأخيرة هي ما ستدور حوله سلسلة الدروس القادمة، وبالرغم من أن المهمة تبدو صغيرة جدا مقارنة بتطوير اللعبة كاملة، إلا أنها على صغرها تحتاج لمجهود معتبر في التصميم والتنفيذ كما سنرى. خطوات سريعة لتبدأ مع محرك Unity إن لم تكن ذا خبرة سابقة بهذا المحرك يمكنك قراءة هذه المقدمة السريعة، كما يمكنك تخطيها إن كنت تعاملت مع هذا المحرك سابقا. لن أطيل شرح هذه الخطوات حيث هناك الكثير من الدروس سواء بالعربية أو الإنجليزية تتناولها، لكنها هنا لنتأكد من أن كل قارئ للسلسلة على نفس الدرجة من المعرفة الأولية قبل البدء. الخطوة الأولى: تحميل وتنصيب المحرك لتنزيل الإصدار الأحدث من المحرك وهو 5 ادخل مباشرة إلى الموقع http://unity3d.com ومن ثم قم بتحميل النسخة المناسبة لنظام التشغيل الذي تستخدمه، علما بأن النسخة المجانية من المحرك ذات إمكانات كبيرة وهي تفي بالغرض بالنسبة لمشروعنا في سلسلة الدروس هذه. الخطوة الثانية: إنشاء المشروع بمجرد تشغيل المحرك بعد تنصيبه ستظهر لك شاشة البداية، قم بالضغط على New Project لتظهر لك شاشة كالتي تراها في الصورة أدناه. كل ما عليك هو اختيار النوع 2D ومن ثم اختيار اسم وموقع المشروع الجديد الذي ستقوم بإنشائه، ومن ثم الضغط على Create Project. الخطوة الثالثة: التعرف على نوافذ البرنامج الرئيسية تهمنا في البداية 4 نوافذ رئيسية في محرك Unity، وفيما يلي ملخص لوظائفها: نافذة المشهد Scene: وهي التي تستخدمها لبناء مشهد اللعبة وإضافة الكائنات المختلفة إليه وتوزيعها في الفضاء ثنائي الأبعاد. تحتوي هذه النافذة مبدئيا على كائن واحد وهو الكاميرا. هرمية المشهد Hierarchy: تحتوي على ترتيب شجري يحوي كافة الكائنات التي تمت إضافتها للمشهد ويساعدك في تنظيم العلاقات بينها، حيث أنه من الممكن أن تضيف كائنات كأبناء لكائنات أخرى بحيث يتأثر الكائن الابن بالكائن الأب كما سنرى. تحتوي هذه النافذة مبدئيا على كائن واحد وهو الكاميرا. مستعرض المشروع Project: يقوم بعرض جميع الملفات الموجودة داخل مجلد المشروع، سواء تلك التي تمت إضافتها للمشهد أم التي لم تُضف. يحتوي المشروع مبدئيا على مجلد واحد يسمى Assets، وسنضيف داخله كافة الملفات والمجلدات الأخرى. نافذة الخصائص Inspector: عند اختيار أي كائن من هرمية المشهد أو نافذة المشهد أو مستعرض المشروع، فإن خصائصه ستظهر في هذه النافذة ويمكنك تغييرها من هناك. استعرضنا في هذا الدرس ما يظهر من واجهة Unity3D للوهلة الأولى، مع مقدمة بسيطة حول صناعة الألعاب، سنشرع في الدروس القادمة في مشروع عملي نتعلم من خلاله كيفية صناعة لعبة كاملة حقيقية. فترقبوا!
  2. قمنا في الدرس السابق بالتعرف على محرك 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 لتغطية أي ثغرة متبقية في أقصى اليمين نتيجة للكسور. إلى هذه النقطة نكون قد انتهينا من بناء الخلفية والأرضية، بالتالي لو حاولنا تشغيل اللعبة الآن سنحصل على النتيجة التالية: قمنا في هذا الدرس بتجهيز الخلفية و الأرضية للعبة كما قمنا بالتحكم في الكاميرا. في الدرس القادم سنقوم بإنشاء الوحدات البنائية للعبة. اقرأ أيضًا تعرف على أشهر لغات برمجة الألعاب
  3. قمنا في الدرس السابق من هذه السلسلة بتجهير مشهد اللعبة و بناء الأرضية والخلفية إضافة إلى إعداد الكاميرا. الآن ننتقل إلى المكونات الرئيسية للعبة وهي الوحدات البنائية كالصخور والحجارة وأيضا الخصوم. ملاحظة: يمكن تحميل الملفات المصدرية لكامل هذه السلسلة عبر حساب أكاديمية حسوب على Github، يمكن أيضا تحميل ملف APK لتجريب اللعبة على أجهزة Android. إنشاء الوحدات البنائية للعبةالنوع الثاني من الكائنات التي سنقوم بإنشائها هي الوحدات البنائية التي تتكون منها المراحل. هذه الوحدات عبارة عن أشكال هندسية منها مربعات ومثلثات ومستطيلات ودوائر. هذه الأشكال أيضا تتميز بأنها مصنوعة من مواد مختلفة، فيمكن أن تكون من المعدن أو الصخر أو الخشب أو الزجاج أو المواد المتفجرة، وهذه الأنواع كلها متوفرة بكل الأشكال في مجموعة الصور. هذه المواد تتفاوت في الوزن والصلابة، إضافة لأن المواد المتفجرة حين تتحطم تحدث انفجارا يؤثر في الوحدات المجاورة و الخصوم. لنبدأ أولا بحصر الأشكال التي سنستخدمها، ومن ثم نرى ما الذي يتوفر لدينا من صور لكل من هذه الأشكال، وبالتالي نتعرف على طريقة صناعة القوالب الخاصة بكل شكل. قوالب الأشكال هذه ستشكل العنصر الرئيسي الذي سنبني منه المراحل المختلفة. إذا استعرضت أحد مجلدات الوحدات البنائية ستجد مجموعة كبيرة من الأشكال المختلفة، مثلا الصورة في الأسفل توضح لنا محتويات المجلد Metal elements والذي يحتوي على صور الوحدات البنائية المعدنية: لاحظ أن هناك مجموعة أشكال رئيسية، وكل واحد منها يأتي مرة فارغا من المنتصف ومرة ممتلئا. إضافة إلى ذلك فإن هناك عدة مراحل لتشقق كل وحدة قبل تحطمها نهائيا. اختصارا للوقت سأختار مجموعة صغيرة من هذه الأشكال وهي الموجودة في الصورة التالية: سنتعرف الآن على الخطوات اللازمة لتحويل كل صورة من هذه الصور إلى وحدة بنائية ذات خصائص فيزيائية، إضافة لجعلها قابلة للتحطم عبر تلقيها للضربات أو تصادمها مع غيرها أو سقوطها على الأرض أو تأثرها بالانفجارات. سأبدأ مع المثلثات لأنها حالة خاصة تحتاج لعناية عند بناء القالب الخاص بها، بينما ستكون الأشكال التالية أسهل. لبناء قالب المثلث متساوي الضلعين والذي تراه أيمن الصورة، علينا أولا أن نضيف كائنا فارغا ونسميه Triangle ومن ثم نضيف إليه المكوّن Sprite Renderer ومكوّن التصادم المسمى Polygon Collider 2D والذي سيظهر مبدئيا على شكل خماسي كما توضح الصورة في الأسفل. بعد إضافة مكوّن التصادم يمكننا سحب صورة المثلث إلى داخل الخانة Sprite في المكوّن Sprite Renderer. أنوه هنا إلى أنه من الضروري إضافة مكوّن التصادم قبل تحديد الصورة، وذلك لأن Unity سيحاول تغيير شكل مكوّن التصادم ليطابق شكل الصورة وهو ما لا يتم بدقة. سنقوم الآن بتغيير شكل مكوّن التصادم يدويا وذلك عبر الضغط على الزر Edit Collider. عند تغيير شكل مكوّن التصادم فإنه يمكننا اختيار أي نقطة من النقاط الخمسة وتحريكها، كما يمكننا اختيار نقطة وحذفها بضغط المفتاح Delete. سنحتاج لحذف نقطتين من الخمسة ومن ثم مطابقة النقاط الثلاث المتبقية على زوايا المثلث ليصبح كما ترى في الصورة، وبطبيعة الحال كلما قربت الصورة أثناء مطابقة الزوايا حصلت على نتيجة أدق وأفضل. بعد أن انتهينا من تجهيز شكل مكوّن التصادم يمكننا تثبيته بالضغط مجددا على Edit Collider ليتم حفظ الشكل الجديد. أصبح لدينا الآن مثلث يمكن أن تصطدم به الأجسام الأخرى، إلا أنه نفسه ثابت ولا يتأثر بأي عوامل فيزيائية. لنجعله نشطا فيزيائيا علينا أن نضيف مكوّن الجسم الصلب المسمى Rigid Body 2D، وهو المكوّن المسؤول عن وضع الكائن تحت تحكم المحاكي الفيزيائي الخاص بمحرك Unity، مما يجعله يتأثر بالجاذبية وقوى الدفع وغيرها. هذا المكوّن يحتوي على مجموعة خصائص أذكر هنا أهمها بالنسبة لنا: الكتلة Mass: كلما زادت يصبح تحريك الجسم أصعب لأن إزاحته ستحتاج لقوة أكبر، لكن هذا لا يؤثر على سرعة سقوطه.مقاومة الهواء لحركة الجسم Linear Drag: كلما زادت سيصبح توقف الجسم عن الحركة بعد زوال القوى الخارجية عنه أسرع.مقاومة الهواء للدوران Angular Drag: كلما زادت سيصبح توقف الجسم عن الدوران بعد زوال القوى الخارجية عنه أسرع.معامل الجاذبية Gravity Scale: الجاذبية الافتراضية هي 9.8 كما هو الحال مع الجاذبية الأرضية، وسيتم ضرب هذه القيمة بالمعامل لحساب الجاذبية الخاصة بهذا الكائن تحديدا.سنقوم بتعديل هذه القيم من أجل تمييز أنواع الوحدات البنائية عن بعضها؛ فمثلا الوحدات المعدنية يجب أن تكون أكبر كتلة من مثيلاتها الخشبية وهكذا. بقي الآن أن نجعل هذه الوحدات تتأثر بالصدمات وتتحطم بعد تلقيها عددا معينا منها؛ وهذه الآلية تتم عبر عدة بريمجات سنتعرف عليها. البداية ستكون مع البريمج الذي يحدد مدى قوة تحمل الوحدة البنائية وسنسميه Breakable. مهمة هذا البريمج هي تلقي الصدمات وإنقاص صحة الكائن بناء عليها؛ ومن ثم تدميره حين تصل صحته إلى صفر. السرد التالي يوضح البريمج المذكور. using UnityEngine; using System.Collections; public class Breakable : MonoBehaviour { //مقدار الصحة الأولية للكائن public float initialHealth = 100; //متغير داخلي لمتابعة تغير قيمة صحة الكائن private float health; //تستدعى مرة واحدة عند بداية التشغيل void Start () { health = initialHealth; } //تستدعى مرة عند تصيير كل إطار void Update ( } //تقوم باستقبال الضربات وإنقاص الصحة بناء عليها public void TakeDamage(float amount){ //الصحة منتهية فلا يمكن تلقي المزيد من الضربات if (health <= 0.0f) { return; } if(health <= amount){ //في هذه الحالة تم تدمير الكائن health = 0.0f; SendMessageUpwards("BreakableDestroyed"); Destroy(gameObject); return; } //قم بتقليل قيمة الصحة بمقدار قوة الضربة health -= amount; //أرسل رسالة تخبر بتغير الصحة SendMessage("DamageTaken", health); } } كل ما يفعله هذا البريمج البسيط هو تخزين قيمة الصحة الأولية في متغير خاص داخلي وذلك ليمنع البريمجات الأخرى من تغييرها (تذكر فصل الاهتمامات). يمكن إنقاص الصحة عن طريق إرسال رسالة لهذا البريمج اسمها TakeDamage والتي تأتي مرفقة بمقدار قوة الضربة التي تلقاها الكائن. تقوم الدّالة باستقبال هذه الرسالة وإنقاص قوة الضربة من صحة الكائن، فإذا وصلت صفرا أو أقل من ذلك تقوم بتدميره. ترسل هذه الدّالة رسالتين هما DamageTaken حين تلقي الضربة وترفق معها قيمة الصحة الجديدة بعد إنقاصها. الرسالة الأخرى هي BreakableDestroyed والتي يتم إرسالها عند وصول الصحة إلى صفر أي عند تدمير الكائن. بالعودة إلى مجموعة الصور، لنتذكر ما قلناه عن الصور المشققة والتي تمثل مراحل تحطم الوحدة البنائية. مثلا بالنسبة للمثلث الذي نستخدمه حاليا لدينا الصورة الأصلية (السليمة) إضافة لصورتين تمثلان المرحلة الأولى والثانية للتشقق كما هو موضح في الصورة التالية: ما سنقوم به الآن هو كتابة بريمج آخر يقوم بالربط بين مقدار صحة الكائن والصورة المعروضة. بما أن لدينا ثلاث صور فإن كل واحدة منها ستمثل ثلث الصحة. فإذا كانت الصحة الكاملة 60 مثلا، فإن الصورة الأولى ستبقى ظاهرة طالما أن الصحة بين 60 و40، فإذا قلّت عن 40 ستظهر الصورة الثانية، وإذا قلّت عن 20 ستظهر الثالثة. وبهذه الطريقة نعطي اللاعب فكرة عن قرب تحطم هذه الوحدة. البريمج المسؤول عن هذا الأمر هو BreakableDislay والموضح في السرد التالي: using UnityEngine; using System.Collections; public class BreakableDislay : MonoBehaviour { //مصفوفة لتخزين صور جميع مراحل التدمير //مرتبة من الحالة الأسوأ للحالة الأفضل public Sprite[] destructionStages; //مرجع لبريمج الوحدة البنائية private Breakable brk; //مرجع لمكوّن تصيير الصورة private SpriteRenderer sr; //متغير لتخزين صحة الكائن الأصلية في البداية private float fullHealth; //يتم استدعاؤها مرة واحدة عند بداية التشغيل void Start () { brk = GetComponent<Breakable>(); fullHealth = brk.initialHealth; sr = GetComponent<SpriteRenderer>(); UpdateDisplay (fullHealth); } //يتم استدعاؤها مرة واحدة عند تصيير كل إطار void Update () { } //تقوم باستقبال رسالة تلقي الضربة وتحديث الصورة بناء على قيمة الصحة الجديدة void DamageTaken(float newHealth){ UpdateDisplay(newHealth); } //تقوم بتغيير الصورة المعروضة بناء على قيمة الصحة المزودة void UpdateDisplay(float health){ //قم بتغيير مرحلة التدمير بناء على //قيمة الصحة الجديدة float healthRatio = health / fullHealth; int maxIndex = destructionStages.Length - 1; int matchingIndex = (int)(healthRatio * maxIndex); sr.sprite = destructionStages[matchingIndex]; } } الدّالة الأهم في هذا البريمج هي ()UpdateDisplay، والتي تقوم باختيار صورة مرحلة التشقق الملائمة لمقدار الصحة المتبقية. لنفترض أن لدينا 3 مراحل مختلفة كما هو الحال مع المثلث الذي نعمل عليه. عندما تتغير صحة الكائن نتيجة لتلقيه ضربة معينة، سيقوم بإرسال الرسالة DamageTaken مرفقا معها قيمة الصحة الجديدة. عند استقبال هذه الرسالة من قبل البريمج BreakableDisplay يقوم باستدعاء الدّالة ()UpdateDisplay مباشرة ويزودها بقيمة الصحة الجديدة، عندها تقوم الدّالة بقسمة الصحة الجديدة على الصحة الكاملة التي سبق تخزينها في fullHealth. بقسمة الرقمين نحصل على نسبة الصحة الجديدة من الصحة الكاملة، ومن ثم نقوم بتحويل هذه النسبة إلى عدد صحيح يمثل القيمة الأقرب لها إذا ما قسمناه على عدد العناصر الكلي في مصفوفة صور مراحل التشقق. مثلا إذا كانت الصحة الكاملة هي 100 والجديدة هي 25 وعدد المراحل 3. فإن 0.25 * 2 = 0.5 وبتحويله إلى عدد صحيح (دون جبر الكسر) سيصبح الناتج صفرا أي صورة آخر مرحلة للتشقق قبل التدمير. البريمجان اللذان أضفناهما حتى الآن يعملان على حساب صحة الكائن وعرض صورة درجة التشقق بناء عليها. وكما رأينا فحين تصل صحة الكائن إلى صفر سيتم تدميره وحذفه من المشهد. حذف الكائن من المشهد سيؤدي إلى اختفائه عن الشاشة بشكل فجائي، وهو أمر غير محبذ حيث لا بد أن نعطي اللاعب شعورا أن الوحدة البنائية قد تحطمت إلى قطع صغيرة. لحسن الحظ فإن مجموعة الصور التي استخدمناها تحتوي على صور قطع حطام لجميع أنواع المواد المستخدمة، وما علينا سوى استخدامها بالطريقة الصحيحة. لنبدأ أولا بضبط الخصائص الفيزيائية لهذه القطع ومن ثم نصنع منها قوالب بعد أن تصبح جاهزة للاستخدام. لنبدأ مع القطع الثلاث الخاصة بالمعدن، والتي ستكون أجزاء الحطام لكل من الوحدات المعدنية والمتفجرة. صور هذه الأجزاء موجودة في المجلد Explosive elements وهي موضحة في الصورة التالية: سنصنع قالبا واحدا بحيث يمكننا استخدامه لجميع صور القطع، ومن ثم نقوم بتحديد الصورة التي سنضعها عليه من خلال البريمج الذي سنراه بعد قليل. ما يلزمنا في قالب القطع المتناثرة مكوّنان: الأول هو Sprite Renderer والذي سيعرض الصورة كما نعرف،والمكوّن الآخر هو الجسم الصلب Rigid Body 2D والذي سيمكننا من إضافة قوة دافعة لتحريك القطعة.بعد إضافة هذين المكوّنين أصبح القالب المطلوب جاهزا ويمكننا إضافته للمشروع. الصورة التالية توضح القيم المفترضة للجسم الصلب الخاص بهذا القالب (لاحظ أننا لم نضف مكوّن تصادم، وذلك لأننا لا نريد أن تصطدم هذه القطع بأي شيء بل نريدها أن تتناثر وتسقط فحسب). البريمج الثالث الذي سنضيفه للقالب هو BreakablePieces، ومهمته استقبال الرسالة BreakableDestroyed والتي تخبر بأن الكائن قد تم تدميره، وبناء عليها يقوم بإضافة مجموعة من قطع الحطام للمشهد وتحريكها في اتجاهات متباعدة. ليس هذا فقط، بل إن هذا البريمج سيكون مسؤولا عن إحداث الانفجار في حالة المواد المتفجرة. لنلق نظرة على البريمج أولا في السرد أدناه، ومن ثم نتناول أهم خطواته ببعض الشرح. using UnityEngine; using System.Collections; public class BreakablePieces : MonoBehaviour { //قالب كائن قطع الحطام public GameObject piecePrefab; //الصور المستخدمة لقطع الحطام public Sprite[] piecesSprites; //كم عدد القطع التي سيتم إنشاؤها؟ public int pieceCount; //مقدار تصغير أو تكبير قطع الحطام الناتجة public float scale = 1.0f; //ما هي قوة الانفجار الناتجة من تناثر القطع؟ //القيمة صفر تعني عدم وجود انفجار public float explosionForce = 0.0f; //تستدعى مرة واحدة عند بداية التشغيل void Start () { } //تستدعى مرة واحدة عند تصيير كل إطار void Update () { } //BreakableDestroyed تقوم باستقبال الرسالة public void BreakableDestroyed(){ //قم بجلب مكوّن التصادم Collider2D myCollider; myCollider = GetComponent<Collider2D>(); //قم بإيجاد حدود مكوّن التصادم Bounds myBounds = myCollider.bounds; //قم بإنشاء العدد المطلوب من قطع الحطام for(int i = 0; i < pieceCount; i++){ GameObject piece = (GameObject) Instantiate(piecePrefab); //قم بوضع الصورة التالية على القطعة الجديدة int index = i % piecesSprites.Length; SpriteRenderer sr = piece.GetComponent<SpriteRenderer>(); sr.sprite = piecesSprites[index]; //قم بتوليد موقع عشوائي ضمن حدود مكوّن التصادم float posX = Random.Range(myBounds.min.x, myBounds.max.x); float posY = Random.Range(myBounds.min.y, myBounds.max.y); //قم بوضع قطعة الحطام في الموقع العشوائي piece.transform.position = new Vector2(posX, posY); //قم بتحديد الكائن الأب لقطعة الحطام بحيث //يكون الكائن الأب للوحدة البنائية التي تم تدميرها piece.transform.parent = transform.parent; //قم بتغيير حجم القطعة بالمقدار المطلوب piece.transform.localScale *= scale; //قم بجلب مكوّن الجسم الصلب لقطعة الحطام Rigidbody2D rb = piece.GetComponent<Rigidbody2D>(); //قم بإضافة قوة دفع صغيرة //لتحريك قطعة الحطام بحيث يكون اتجاه القوة من مركز //الجسم الأصلي إلى الموقع العشوائي حيث القطعة Vector2 force = piece.transform.position - transform.position; if(explosionForce == 0.0f){ //لا يوجد انفجار rb.AddForce (force, ForceMode2D.Impulse); } else { //يوجد انفجار rb.AddForce(force * explosionForce, ForceMode2D.Impulse); } //احذف قطعة الحطام من المشهد بعد 5 ثوان Destroy(piece, 5.0f); } if (explosionForce > 0.0f) { //تأثير الانفجار على الأجسام الأخرى //قم بحساب نصف قطر مجال تأثير الانفجار float radius = myBounds.max.magnitude * 2.0f; //قم بإيجاد جميع الوحدات البنائية والأجسام القابلة للتحطيم Breakable[] breakables = FindObjectsOfType<Breakable>(); //قم بإرسال قوة الانفجار كمقدار تدمير للأجسام الواقعة في محيط التأثير foreach (Breakable target in breakables) { //استثن الكائن نفسه الذي نتج عنه الانفجار if (target.gameObject != gameObject) { //قم بحساب المسافة بين الكائن وموقع الانفجار وقارنها بنصف قطر محيط التأثير float dist= Vector2.Distance( transform.position, target.transform.position); if (dist <= radius) { //قم بإضافة قوة الانفجار كمقدار للتدمير target.TakeDamage(explosionForce / dist); //قم بحساب مقدار واتجاه قوة دفع الانفجار Vector2 expDir = target.transform.position - transform.position; expDir = expDir * (explosionForce / radius); //قم بإضافة قوة الانفجار للكائن Rigidbody2D targetRB = target.GetComponent<Rigidbody2D>(); targetRB.AddForce(expDir, ForceMode2D.Impulse); } } } } } } يحتاج هذا البريمج حتى يعمل إلى قالب يستخدمه من أجل إنشاء قطع الحطام المطلوبة، إضافة إلى قائمة بالصور التي يمكنه استخدامها لتمثيل هذه القطع. بطبيعة الحال ستختلف قائمة الصور هذه تبعا لمادة الوحدة البنائية الأصلية؛ فالكائن المعدني سينتج قطعا معدنية والخشبي قطعا خشبية وهكذا. إضافة لذلك يمكننا تحديد عدد القطع التي ستنتج عن تحطيم كل وحدة بنائية وحجم هذه القطع. هذه الأرقام ستمكننا مع ضبط عدد وحجم القطع بما يتناسب مع حجم الشكل الأصلي. أخيرا يمكننا اختيار وجود انفجار من عدمه، ومدى قوة الانفجار إن وجد. هذا يجعل البريمج قابلا للاستخدام مع الوحدات المتفجرة أيضا. كل خطوات هذا البريمج مرتبطة باستقبال الرسالة BreakableDestroyed التي سبق ذكرها. عند استقبال الرسالة يقوم البريمج باستخراج مكوّن التصادم ومن ثم استخراج حدود هذا المكوّن، وهي نفسها حدود الكائن. بعد ذلك يدخل في حلقة تكرارية لإنتاج العدد المطلوب من القطع، حيث يقوم في كل مرة بإنشاء قطعة جديدة واختيار صورة لها بالتسلسل. لاحظ استخدام باقي ناتج القسمة من أجل ضمان العودة من بداية قائمة الصور في حال كان عدد القطع المطلوبة أكبر من الصور المتوفرة. بعدها يتم توليد موقع عشوائي لوضع قطعة الحطام، شرط أن يكون ضمن الحدود التي سبق استخراجها من مكوّن التصادم. بما أنّ الكائن الأصلي سيتم حذفه في نهاية الإطار الحالي، فإنه لا يصلح ليكون أبا لقطع الحطام لأنها ستحذف معه في هذه الحالة. بالتالي نأخذ أب الكائن الأصلي ونحدده كأب لقطع الحطام. كذلك يتم تغيير حجم القطعة بالمقدار المطلوب scale، حيث نحتاج أحيانا لجعلها أكبر أو أصغر من حجمها في الصورة الأصلية. بعد أن تصبح قطعة الحطام جاهزة وفي مكانها الصحيح، سنحتاج لإضافة قوة فيزيائية تحركها بعيدا عن مركز القطعة الأصلية. نقوم أولا باستخراج مكوّن الجسم الصلب Rigid Body 2D وهو المسؤول عن الخصائص الفيزيائية لقطعة الحطام، ومن ثم نحسب المتجه المطلوب لهذه القوة وذلك بطرح موقع قطعة الحطام من موقع الكائن الأصلي. إذا كانت قوة الانفجار تساوي صفرا فلن نحتاج سوى لإضافة هذه القوة للجسم الصلب على شكل قوة اندفاع Impulse. أما في حالة وجود انفجار فالأمر أكثر تعقيدا، حيث يتوجب علينا أولا ضرب القوة الدافعة بمقدار قوة الانفجار مما ينتج عنه اندفاع الحطام بسرعة أكبر ولمسافة أبعد. إضافة لذلك سيكون علينا إحداث تأثير على القطع المجاورة وهذا الأمر يتم عن طريق حساب نصف قطر محيط تأثير الانفجار، والذي اخترناه هنا ليكون طول متجه الحد الأقصى مضروبا في 2. نقوم بعدها باستخدام الدّالة ()FindObjectsOfType من أجل البحث عن جميع الكائنات التي تحتوي على البريمج Breakable (أي الوحدات البنائية)، وأي واحد من هذه الوحدات يقع في محيط التأثير يجب أن تضاف له قوة الانفجار الدافعة متناسبة عكسيا مع المسافة بين مركز الانفجار في الجسم المدمر وبين الوحدة المستهدفة. قوة الانفجار الدافعة ستقوم فقط بتحريك الوحدة البنائية، لكننا نريد أيضا إنقاص صحتها بسبب الانفجار ولذلك نستدعي ()TakeDamage. بعد إضافة البريمجات أصبح الكائن جاهزا لنصنع منه قالب المثلث المعدني. لكون عدد قوالب الأشكال كبيرا نسبيا، من الأفضل وضع كل مجموعة في مجلد خاص بها. مثلا مجلد Metal للأشكال المعدنية و Wood للخشبية وهكذا. بعد صناعة القالب يمكنك أن تغير الكائن نفسه وتغير اسمه ومن ثم تسحبه مرة أخرى إلى مستعرض المشروع لصناعة قالب جديد. مثلا يمكنك استبدال صورة المثلث بالدائرة ومن ثم تغيير الاسم إلى Circle. أيضا سيتوجب عليك تغيير شكل مكوّن التصادم عن طريق إزالة المكوّن Polygon Collider 2D وإضافة المكوّن Circle Collider 2D لأنه الأنسب للدائرة. وبعد صناعة قالب الدّائرة نضيف واحدا آخر للمستطيل مع تغيير مكوّن التصادم مرة أخرى ليصبح Box Collider 2D وهكذا حتى ننجز الأشكال التي نرغب بها. أثناء عملك راعي الأمور التالية: لحذف مكوّن موجود على كائن، قم بالضغط على رمز الترس في أعلى يمين المكوّن في نافذة الخصائص ومن ثم اختر Remove Component.هناك بعض الأشكال مثل المثلث القائم تحتوي على صور لمرحلتي تشقق فقط بينما الأشكال الباقية تحتوي على 3 مراحل. هذا يؤثر فقط على عدد عناصر المصفوفة destructionStages ولا يلزمنا تغيير أي شيء في الكود.مقدار الكتلة للمواد هي 6 للمعدن والمواد المتفجرة و2.5 للزجاج و5 للصخور و1 للخشب.مقدار الصحة الافتراضية هي 100 للمعدن و 45 للمواد المتفجرة و20 للزجاج و 80 للصخور و 35 للخشب.بعد الانتهاء من الوحدات البنائية علينا أن نحل مشكلة صغيرة ستطرأ الآن. تذكر أن الكائنات الموجودة في المشهد حاليا هي الخلفية والأرضية والوحدات البنائية، وجميع هذه الكائنات تحتوي على مكوّنات تصادم بغض النظر عن أشكالها. معنى هذا أن Unity سيقوم باكتشاف التصادمات بين هذه الكائنات مع بعضها، وهذا ما نريده بالنسبة للأرضية والوحدات البنائية التي ستوضع فوقها، لكننا لا نريده للخلفية. فإذا تصادمت الوحدات البنائية مع الخلفية سيختل منطق المشهد تماما. لحل هذه المشكلة يوفر لنا Unity نظام الطبقات Layers والذي يمكننا من خلاله فصل الكائنات إلى طبقات بحيث يمكننا لاحقا التحكم بعلاقات هذه الطبقات مع بعضها. فمثلا يمكن أن نحدد للكاميرا ما هي الطبقات التي يمكنها أن تراها، كما يمكننا أن نحدد أي الطبقات تتصادم مع بعضها البعض. إذن لحل هذا الإشكال علينا أن نضيف طبقة جديدة مخصصة للخلفية، ونمنعها من التصادم مع الطبقة الافتراضية التي تنتمي إليها باقي الكائنات. لإضافة طبقة جديدة قم باختيار قالب الخلفية BGElement ومن ثم افتح القائمة Layer الموجودة في أعلى يمين نافذة الخصائص واختر Add Layer كما ترى في الصورة: ستظهر لك نافذة فيها مصفوفة بجميع الطبقات الافتراضية حيث يمكنك من خلالها إضافة طبقات جديدة خاصة بك. لنقم بإضافة طبقة جديدة اسمها Background، ومن ثم نقوم بتغيير القالب BGElement ليصبح ضمن هذه الطبقة كما ترى في الصورتين التاليتين: بعدها علينا أن نطلب من محاكي الفيزياء الخاص بالمحرك أن يهمل التصادمات الحاصلة بين الطبقة الافتراضية Default وطبقة الخلفية Background بحيث لا تتصادم عناصر الأرضية أو الوحدات البنائية مع الخلفية. لتنفيذ هذا الأمر اذهب إلى القائمة: Edit > Project Settings > Physics 2D حيث ستجد في أسفل نافذة الخصائص مصفوفة تصادم الطبقات Layer Collision Matrix والتي تحتوي على جميع الطبقات الموجودة في المشروع منتظمة في صفوف وأعمدة. في كل نقطة تقاطع بين صف وعمود ستجد مربعا يمكنك تفعيله أو تعطيله. إذا قمت بتعطيل أحد هذه المربعات فإن التصادم الحاصل بين الطبقتين المتقاطعتين فيه سيتم إهماله. بالتالي ما نريده هو تعطيل المربع الذي تتقاطع فيه الطبقة Default مع الطبقة Background كما ترى في الصورة أدناه: بعد تجهيز قوالب الوحدات البنائية يتبقى علينا إضافة قالب قطع الحطام للبريمج BreakablePieces، كما أن علينا تحديد صور القطع الخاصة بكل وحدة بنائية وتحديد عددها وأحجامها. لاختصار الوقت سأعتمد قيما ثابتة لجميع الوحدات البنائية، وهو 5 قطع حطام لكل وحدة وتحجيم بمقدار 0.5 (صور قطع الحطام موجودة في مجلد يسمى Debris). للوقوف على التقدم في بناء قالب الوحدة البنائية، يفترض أن يكون ما أنشأناه حتى الآن شبيها بما تراه في الصورة أدناه: السؤال التالي الذي نريد الإجابة عليه هو: كيف تتناقص قيمة صحة هذه الوحدات البنائية عند سقوطها أو اصطدامها ببعضها البعض أو بمقذوفات اللاعب؟ يساعدنا Unity بالإجابة على هذا السؤال حيث أنه يوفر لنا رسائل إعلام بحدوث أي عمليات تصادم بين الكائنات المختلفة. سنقوم الآن بكتابة بريمج يستقبل رسائل التصادم هذه ويقوم بتحليل محتوياتها وحساب مقدار التدمير الذي يجب أن يتم على كل وحدة بنائية. هذا البريمج هو CollisionDamageTaker، وهو موضح في السرد التالي: using UnityEngine; using System.Collections; public class CollisionDamageTaker : MonoBehaviour { //متغير لتخزين آخر عملية تصادم //يساعد هذا على منع تكرار التصادمات private float lastCollisionTime = 0.0f; //يتم استدعاؤها مرة واحدة عند بداية التشغيل void Start () { } //يتم استدعاؤها مرة عند تصيير كل إطار void Update () { } //يتم استدعاؤها عند حدوث تصادم بين هذا الكائن وكائن آخر void OnCollisionEnter2D(Collision2D col) { if (Time.time - lastCollisionTime < 0.25f) { //لم يمر وقت كاف منذ آخر تصادم، قم بإهمال هذا التصادم ولا تفعل شيئا return; } //قم بتسجيل وقت حدوث التصادم lastCollisionTime = Time.time; //قم باستخراج مكوّن الموقع من الكائن الآخر الذي حدث معه التصادم Transform otherObject = col.transform; //احسب ارتفاع الكائنين المتصادمين float myHeight = transform.position.y; float otherHeight = otherObject.position.y; //ما هو مقدار قوة الإصابة التي تم تلقيها؟ float damageAmount = 1.0f; //قم بحساب قوة الإصابة بناء على موقع //الكائن الآخر if (myHeight < otherHeight) { //سقط كائن آخر فوق هذا الكائن //كم كانت سرعة الجسم الساقط حين اصطدامه؟ float fallVelocity = col.relativeVelocity.magnitude; //اضرب مقدار الإصابة بسرعة الاصطدام damageAmount *= fallVelocity; //ما هي كتلة الكائن الذي سقط فوق هذا الكائن؟ Rigidbody2D otherRB = otherObject.GetComponent<Rigidbody2D>(); float otherMass = otherRB.mass; //اضرب مقدار الإصابة مرة أخرى بالكتلة damageAmount *= otherMass; //قم أخيرا بمضاعفة مقدار الإصابة لأن الكائن الآخر جاء من مكان أعلى damageAmount *= 2.0f; } else { //الكائن الحالي هو الذي سقط //الكائنات الساقطة بسرعة أكبر ستحصل على إصابة أكبر float myFallVelocity = col.relativeVelocity.magnitude; //اضرب مقدار الإصابة بسرعة الاصطدام damageAmount *= myFallVelocity; } //قم بإرسال رسالة لإنقاص الصحة بمقدار الإصابة المحسوبة SendMessage("TakeDamage", damageAmount); } } كما ذكرت فإنّ هذا البريمج يعمل على تقليل صحة كائن الوحدة البنائية بناء على اصطدامها بالكائنات الأخرى في المشهد. المتغير الوحيد الذي عرفناه هنا هو lastCollisionTime والذي يخزن الوقت الذي تم فيه إنقاص الصحة آخر مرة. الهدف من استخدام هذا المتغير هو منع الاصطدامات المتتالية والتي قد تحدث في إطار واحد أو إطارين متتابعين من إنقاص الصحة بسرعة كبيرة، حيث أننا هنا نسمح بإنقاص الصحة مرة كل ربع ثانية فقط. عند تصادم كائنين مع بعضهما يقوم محرك Unity بإرسال الرسالة OnCollisionEnter إلى هذين الكائنين في نفس الوقت، ويرفق مع هذه الرسالة متغيرا من نوع Collision2D والذي يحتوي على معلومات حول التصادم، إضافة لأنه يحتوي على مرجع للكائن الآخر بالتالي يمكن لكل منهما التعرف على الكائن الآخر الذي اصطدم به وخصائصه. الخطوة الأولى للدّالة ()OnCollisionEnter هي أن تتأكد من مرور ربع ثانية على الأقل منذ آخر مرة تم فيها إنقاص صحة الكائن، فإن لم يتحقق هذا الشرط سيتوقف تنفيذ الدّالة على الفور. طريقة حساب مقدار الإصابة (المقدار الذي سيتم إنقاصه من الصحة) يعتمد على عدة عوامل ويبدأ من قيمة افتراضية هي 1. العامل الأول هو ارتفاع كل من الكائنين المتصادمين. لنفترض أن الدّالة اكتشفت أثناء التنفيذ أن الكائن الآخر otherObject ذو ارتفاع أعلى من الكائن الحالي. هذا الأمر يفسر على أن الكائن الآخر قد سقط فوق الكائن الحالي واصطدم به، بالتالي يجب أن يكون مقدار الإصابة أعلى. من أهم المعلومات التي تحملها الرسالة هي col.relativeVelocity، والتي تحسب السرعة النسبية بين الكائنين لحظة الاصطدام. منطقيا يجب أن يتناسب مقدار هذه السرعة طرديا مع قوة الإصابة، لذا فإننا نقوم باستخراج هذا المقدار relativeVelocity.magnitude (السرعة هنا هي متجه لذلك نستخرج مقدارها) وضربه بقيمة قوة الإصابة. بعد ذلك نقوم باستخراج مكوّن الجسم الصلب RigidBody2D من الكائن الآخر وضرب كتلته بمقدار الإصابة، حيث يجب أن تزيد الإصابة كلما كان الكائن الذي سقط ذا كتلة أكبر. أخيرا نقوم بمضاعفة القيمة النهائية التي حسبناها؛ وذلك لجعل سقوط كائن فوق آخر ذا تأثير أكبر من سقوط الكائن نفسه على الأرض أو على جسم آخر. الاحتمال الآخر هو أن يكون الكائن الحالي ذا ارتفاع أعلى من الكائن الآخر الذي سقط فوقه، بالتالي لا حاجة لنا بأخذ أية معلومات من هذا الجسم الآخر. كل ما سنفعله في هذه الحالة هو ضرب قيمة الإصابة بسرعة الاصطدام، مما يجعل سقوط الجسم نفسه أقل أذى من سقوط جسم آخر فوقه. بعد المرور على أحد هذين الاحتمالين نحصل على قيمة الإصابة النهائية في المتغير damageAmount وبالتالي نرسل الرسالة TakeDamage ونرفق معها القيمة المحسوبة. لاحظ أن البريمج المرسل CollisionDamageTaker والمستقبل Breakable موجودان على نفس الكائن، ولا يوجد أي كائن آخر مهتم باستقبال هذه الرسالة. لهذا السبب استخدمنا الدّالة()SendMessage في عملية الإرسال. بقي علينا خطوة واحدة لإكمال قالب الوحدة البنائية، وهي الصوت الذي سيصدر من الوحدة البنائية حين تحطمها. هذا الصوت سيختلف باختلاف مادة الوحدة البنائية؛ فصوت تحطم الزجاج ليس كالحجر وليس كالمعدن أو الخشب. هذا الاختلاف سينعكس على ملف الصوت المستخدم، إلا أن التقنية ستكون نفسها وستعمل كلها باستخدام نفس البريمج وهو BreakableSounds. سيعمل هذا البريمج على إصدار صوت في حالتين: عند تلقي إصابة بمقدار قوي، وعند تحطم الوحدة البنائية نهائيا. الهدف من حصر الصوت الأول بالإصابات ذات المقدار القوية هو أن عددا كبيرا من التصادمات يمكن أن يحدث في نفس الإطار مما سينتج عنه أصوات كثيرة مزعجة وهي نتيجة ليست مرضية على الإطلاق. هذا البريمج موضح في السرد التالي: using UnityEngine; using System.Collections; public class BreakableSounds : MonoBehaviour { //يتم تشغيل هذا الصوت عند تلقي إصابة public AudioClip damageSound; //يتم تشغيل هذا الصوت عند تحطم الوحدة البنائية public AudioClip destructionSound; //الحد الأدنى لمقدار الإصابة التي تؤدي لتشغيل الصوت public float minDamage = 25.0f; //المدة الزمنية التي يمنع خلالها تشغيل أي صوت من قبل هذا الكائن private float suspensionTime = 0.0f; //الوقت الذي بدأ فيه تشغيل آخر صوت من قبل هذا الكائن private float playTime = 0.0f; //تستدعى مرة واحدة عند بداية التشغيل void Start () { } //تستدعى مرة عند تصيير كل إطار void Update () { } //DamageTaken تقوم باستقبال رسالة الإصابة void DamageTaken(float amount) { //سيتم تشغيل الصوت فقط للإصابات التي //يزيد مقدارها عن الحد الأدنى if (amount >= minDamage) { CheckAndPlay(damageSound); } } //BreakableDestroyed تقوم باستقبال رسالة تدمير الوحدة البنائية void BreakableDestroyed() { AudioSource.PlayClipAtPoint(destructionSound, transform.position); } //تقوم بحساب الوقت الذي يمنع فيه تشغيل ملف الصوت وتقرر بناء عليه تشغيل الملف مجددا من عدمه void CheckAndPlay(AudioClip clip) { if (Time.time - playTime > suspensionTime) { AudioSource.PlayClipAtPoint(clip, transform.position, 0.25f); playTime = Time.time; suspensionTime = clip.length + 0.5f; } } } لعل المهام الرئيسية لهذا البريمج واضحة: فهو يستقبل الرسالتين DamageTake و BreakableDestroyed من البريمج Breakable ويقوم بناء عليها بتشغيل واحد من ملفي الأصوات المحددين وهما من نوع AudioClip، واللذين يمكننا اختيارهما من نافذة الخصائص كما سنرى بعد قليل. المتغير minDamage يمكننا من اختيار الحد الأدنى للإصابة التي ستؤدي إلى تشغيل ملف الصوت، وهو افتراضيا 25. لاحظ أن تشغيل صوت الإصابة يتم بطريقة تختلف عن تشغيل صوت التدمير؛ ذلك أن الأخير لا يمكن أن يتكرر كما هو الحال في الأول. بطبيعة الحال فإن تلقي عدة إصابات في فترة قصيرة أمر ممكن، لذلك علينا أن نمنع تكرار تشغيل ملف الصوت مرات متتابعة بشكل مزعج. لأجل ذلك نقوم عند تشغيل ملف صوت الإصابة بحساب طول الملف وإضافة نصف ثانية إليه، ومن ثم تخزين الناتج في المتغير suspentionTime. عند محاولة تشغيل ملف صوت الإصابة مرة أخرى نقوم بمقارنة الوقت المنقضي منذ آخر تشغيل بوقت منع التشغيل suspentionTime. إذا كانت هذه المدة أقل من وقت المنع فلن يتم تشغيل الملف. هذه الخطوات تقوم بها الدّالة ()CheckAndPlay. بعد إضافة هذا البريمج لقالب الوحدة البنائية، سيظهر المتغيران damageSound و destructionSound في نافذة الخصائص على شكل خانات يمكن أن تضيف إليها ملفات صوتية. لكل نوع من أنواع الوحدات البنائية الخمسة (المعدن والمتفجرات والحجر والخشب والزجاج) سيكون هناك ملفات مختلفة. هذه الملفات تم جلبها من مواقع مختلفة حيث لا يتوفر دائما مكتبة صوتية شاملة يمكن الاعتماد عليها كمصدر لكل المؤثرات الصوتية، أو على الأقل لا تتوفر مجانا. كما ذكرت سابقا تم وضع الملفات في مجلدات تحمل أسماء المواقع التي تم إحضارها منها، مع الإبقاء على اسم الملف الأصلي كما هو في الموقع دون تغيير. الصورة التالية توضح شكل البريمج BreakableSounds لكائنين أحدهما وحدة بنائية خشبية والأخرى حجرية. وبهذا نكون قد انتهينا من قوالب الوحدات البنائية بشكل عام. الصورة الأخيرة في الأسفل توضح الشكل العام لقوالب الوحدات البنائية مع كافّة المكوّنات اللازمة، والتي تم إغلاقها اختصارا للمساحة. بقي أمر واحد سنضيفه كلمسة جمالية على لعبتنا، وهي حدوث انفجار مرئي عند تحطم الوحدات البنائية المتفجرة. هذا الانفجار موجود في الحزمة Explosion Pack في موقع kenney.nl والذي سبق وضع روابطه، وهو يحمل الاسم Regular Explosion. الانفجار عبارة عن 9 صور متتابعة كما ترى في الصورة في الأسفل: ما يتوجب فعله الآن هو أن نصنع قالبا خاصا بالانفجار بحيث يعرض هذه الصور التسعة بشكل متتابع، ومن ثم نقوم بكتابة بريمج يعمل على إنشاء كائن من هذا القالب في حال تحطم أحدى الوحدات البنائية المتفجرة. لنبدأ مع قالب الانفجار والذي سيحتوي على مكوّن واحد هو SpriteRenderer إضافة للبريمج SpriteAnimator الموضح في السرد التالي، والذي سيعمل على عرض الصور بشكل متتابع. using UnityEngine; using System.Collections; public class SpriteAnimator : MonoBehaviour { //مصفوفة تحتوي على اللقطات التي تشكل الحركة public Sprite[] frames; //سرعة التحريك مقدرة بعدد اللقطات في الثانية public float framesPerSecond = 16.0f; //هل يجب أن يتم تدمير الكائن وحذفه بعد انتهاء التحريك؟ public bool destroyOnCompletion = true; //متغير لتسجيل الوقت الذي تم فيه تغيير اللقطة آخر مرة private float lastChange = 0.0f; //متغير مرجعي لمكوّن تصيير الصور ثنائية الأبعاد private SpriteRenderer renderer; //موقع اللقطة المعروضة حاليا في مصفوفة اللقطات private int currentFrame = 0; //يتم استدعاؤها مرة واحدة عند بداية التشغيل void Start () { renderer = GetComponent<SpriteRenderer>(); } //يتم استدعاؤها مرة عند تصيير كل إطار void Update () { Animate(); } //تقوم بعملية التحريك عن طريق عرض اللقطات بشكل متتابع void Animate() { //احسب المدة التي يجب أن تقضيها اللقطة الواحدة float frameTime = 1.0f / framesPerSecond; //قم بالتبديل للإطار التالي في حال انقضى الوقت اللازم للقطة السابقة if (Time.time - lastChange > frameTime) { lastChange = Time.time; currentFrame = (currentFrame + 1) % frames.Length; renderer.sprite = frames[currentFrame]; if (currentFrame == 0 && destroyOnCompletion) { Destroy(gameObject); } } } } ببساطة شديدة يقوم هذا البريمج عند تصيير كل إطار باستدعاء الدّالة Animate، والتي تقوم بحساب الوقت اللازم لعرض كل لقطة عن طريق قسمة الرقم 1 على السرعة. بعد ذلك تقارن الوقت المنقضي منذ تم التبديل للقطة الحالية مع الوقت اللازم للقطة الواحدة، فإذا انقضى هذا الوقت تقوم بالتبديل إلى اللقطة التالية في المصفوفة frames. من البديهي هنا أن تتم إضافة لقطات الانفجار بشكل مرتب إلى المصفوفة frames وذلك حتى تظهر بالتتابع الصحيح كما هو موضح في الصورة التالية. لاحظ أخيرا أننا نقوم بتدمير الكائن بعد انتهاء عرض اللقطات لأننا لا نريد تكرار مشهد الانفجار سوى مرة واحدة. بعد إنشاء قالب الانفجار علينا أن نربط إنشاء الانفجار بتدمير الوحدة البنائية المتفجرة. هذه عملية بسيطة لا تتطلب أكثر من استقبال الرسالة BreakableDestroyed ومن ثم إنشاء كائن جديد من قالب الانفجار في نفس موقع الوحدة البنائية التي تم تدميرها. هذه المهمة يقوم بها البريمج BreakableSpawn والموضح في السرد التالي: using UnityEngine; using System.Collections; public class BreakableSpawn : MonoBehaviour { //قالب الكائن الذي سيتم إنشاؤه بعد التدمير public GameObject prefab; //يتم استدعاؤها مرة عند بداية التشغيل void Start () { } //يتم استدعاؤها مرة عند تصيير كل إطار void Update () { } //BreakableDestroyed تقوم باستقبال رسالة تدمير الوحدة البنائية void BreakableDestroyed() { //قم بإنشاء كائن جديد كم القالب وضعه في نفس موقع الكائن الحالي الذي تم تدميره GameObject newObject = (GameObject)Instantiate(prefab); newObject.transform.position = transform.position; newObject.transform.parent = transform.parent; } } إنشاء كائنات الخصومكما تعلم تتكون المرحلة الواحدة في هذه الألعاب من العناصر البنائية والخصوم ومقذوفات اللاعب التي يطلقها على هذه الوحدات والخصوم التي تحتمي بها. بالتالي فالعنصر الثاني الذي يلزمنا بعد تجهيز الوحدات البنائية هو شخصيات الخصوم. بالاطلاع على المجلد Aliens في مجموعة الصور، تلاحظ وجود خمسة أنواع من الكائنات بأشكال غريبة، بعضها مربع الشكل والآخر دائري، وبعضها يرتدي ما يشبه الخوذة. من ناحية السلوك الفيزيائي لا تختلف كائنات الخصوم عن الوحدات البنائية؛ حيث أنها تمتلك صحة محددة تنقص بتلقي الضربات، كما أنها تتأثر بالانفجارات والتصادمات وما شابه. هذا يعني – لحسن الحظ – أن المجهود الذي بذلناه في كتابة البريمجات السابقة الخاصة بقوالب الوحدات البنائية سيعطي ثماره في هذه المرحلة، حيث سنعيد استخدام تلك البريمجات من أجل صنع قوالب الخصوم، ولن نحتاج لكتابة أي بريمج جديد باستثناء واحد سنتحدث عنه بعد قليل. كل ما علينا هو إضافة البريمجات Breakable و BreakablePieces و CollisionDamageTaker و BreakableSounds، ومن ثم تحديد القيم المناسبة لكل نوع من أنواع الوحوش. اختصارا للوقت سأعتبر الأشكال الأربعة ذات نفس الخصائص، إضافة لمنح الوحوش التي تحتوي على خوذة مقدارا أعلى من الصحة. ستكون صحة الوحشين المربع والدائري هي 50، بينما الوحش ذو الخوذة ستكون صحته 100. وهذا سينطبق على الألوان الخمسة للوحوش. بطبيعة الحال علينا أيضا إضافة مكوّن الجسم الصلب RigidBody2D و أحد مكوّني التصادم BoxCollider2D أو CircleCollider2D وذلك حسب شكل الوحش. الشكل التالي يوضح القالب الخاص بأحد الوحوش التي ترتدي خوذة. يمكنك الاطلاع على قوالب الوحوش في المجلد Prefabs\Enemies: هناك اختلافات أخرى يجب مراعاتها بين الوحدات البنائية والوحوش، فهناك ملفات صوتية مختلفة للإصابة والتدمير، إضافة لأن تدمير الوحوش ينتج عنه نجوم بدلا من قطع الحطام. صور النجوم هذه موجودة في المجلد PhysicsPack\Others وهي موضحة في الشكل التالي. لاحظ أيضا أن عدد النجوم الناتجة كبير نسبيا وهو 10: كل الفروق السابقة بين الوحدات البنائية والوحوش هي فروق شكلية، لكن الفرق الأهم هو أن نكون قادرين على التمييز بينهما برمجيا؛ وذلك لأن تدمير الوحوش يختلف عن تدمير الوحدات البنائية وهو ذو علاقة مباشرة بحالة اللعبة. فتدمير جميع الوحوش في المشهد سيؤدي لفوز اللاعب بالمرحلة وانتقاله للمرحلة التالية، إلا أن هذا ليس هو الحال بالنسبة للوحدات البنائية. ولأن تدمير الوحوش أمر مهم، علينا أن نضيف لقالب كل وحش منها بريمجا يقوم بإرسال رسالة للكائن الجذري حين تدمير كائن الوحش، وسيتكفل بريمج آخر سنأتي عليه بعد حين بمهمة استقبال الرسالة وعمل ما يلزم حيالها. ما يهم الآن هو أن نضيف البريمج Enemy والموضح في السرد التالي لكل قوالب الوحوش التي أنشأناها. using UnityEngine; using System.Collections; public class Enemy : MonoBehaviour { //يتم استدعاؤها مرة عند بداية التشغيل void Start () { } //يتم استدعاؤها مرة عند تصيير كل إطار void Update () { } //ومن ثم BreakableDestroyed تقوم باستقبال الرسالة //ترسل رسالة للأعلى أي للكائن الجذري بأن الوحش تم تدميره void BreakableDestroyed() { SendMessageUpwards("EnemyDestroyed"); } }:بهذا أصبح لدينا كل ما يلزم لبناء مشهد، وما عليك سوى رص الوحدات البنائية والوحوش بأي ترتيب ترغب به من أجل صنع مرحلة كما في الصورة التالية .وبهذا نكون انتهينا من تجهيز مشهد اللعبة. سنقوم في الدرس القادم ببناء واجهة المستخدم والشاشة الرئيسية للعبة
×
×
  • أضف...