مدخل إلى unity3d إنشاء مقذوفات اللعبة وآلية إطلاقها في Unity3D


ياسر جفال

إنشاء المقذوفات

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

0.thumb.png.c0db3cd18c89cbe22fa60c5eacd8

علينا أن نقوم ببناء قالب مقذوف لكل صورة من هذه الصورة الأربعة. هذا القالب سيحتوي مبدئيا على المكوّن Sprite Renderer والذي أصبح معروفا لدينا، إضافة للمكوّنين Rigid Body 2D و Circle Collider 2D. هذه المكوّنات كفيلة بتحويل كل صورة إلى كائن نشط فيزيائيا، وكل ما يلزمنا هو تعديل قيم الكتلة في مكوّن الجسم الصلب لتصبح 5 لكل واحد من هذه الصور، إضافة لتفعيل الخيار Is Kinematic والذي يمنع استجابة الجسم الصلب للقوى الخارجية، وهو ما يلزمنا في البداية وسنقوم بتغييره لاحقا. الكتلة الكبيرة ضرورية لجعل هذه المقذوفات تحدث تأثيرا ملحوظا حين تصطدم بالوحدات البنائية أو الخصوم حال إطلاقها. بعد ذلك علينا أن نبدأ في كتابة وإضافة البريمجات اللازمة لهذه المقذوفات. البداية ستكون مع البريمج الرئيسي والأهم وهو Projectile. والموضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class Projectile : MonoBehaviour {

//عدد الثواني التي سيعيشها المقذوف في المشهد بعد إطلاقه
public float lifeSpan = 7.5f;

//هل يُسمح للاعب بالتحكم بهذا المقذوف حاليا؟
private bool controllable = false;

//هل قام اللاعب بإمساك هذا المقذوف وإعداده للإطلاق؟
private bool held = false;

//هل تم إطلاق هذا المقذوف بالفعل؟
private bool launched = false;

//هل تم تنفيذ الهجوم الخاص بهذا المقذوف؟
private bool attackPerformed = false;

//متغير لتخزين موقع المقذوف لحظة إمساك اللاعب له وقبل سحبه استعدادا للإطلاق
private Vector2 holdPosition;

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

}

//يتم استدعاؤها مرة عند تصيير كل إطار
void Update () {

}

//تسمح للاعب بالتحكم بهذا المقذوف شريطة ألا يكون قد تم إطلاقه فعلا
public void AllowControl()
{
    if (!launched)
    {
        controllable = true;
    }
}

//تستدعى عند بداية إمساك اللاعب للمقذوف استعدادا لإطلاقه
public void Hold()
{
    if (controllable && !held && !launched)
    {
        held = true;
        holdPosition = transform.position;
        //أرسل رسالة تخبر بإمساك اللاعب للمقذوف
        SendMessage("ProjectileHeld");
    }
}

//تقوم بإطلاق المقذوف مستخدمة مقدار القوة المزود
public void Launch(float forceMultiplier)
{
    if (controllable && held && !launched)
    {
        //احسب متجه الإطلاق
        Vector2 launchPos = transform.position;
        Vector2 launchForce = (holdPosition - launchPos) * forceMultiplier;
        //أضف قوة الإطلاق المحسوبة للجسم الصلب
        Rigidbody2D myRB = GetComponent<Rigidbody2D>();
        myRB.isKinematic = false;
        myRB.AddForce(launchForce, ForceMode2D.Impulse);
        //قم بضبط متغيرات الحالة الجديدة
        launched = true;
        held = false;
        controllable = false;
        //قم بتدمير المقذوف بعد انقضاء الثواني المحددة لبقائه في المشهد
        Destroy(gameObject, lifeSpan);
        //أرسل رسالة تخبر بحدوث الإطلاق
        SendMessage("ProjectileLaunched");
    }
}

//تقوم بتنفيذ الهجوم الخاص لهذا المقذوف بعد إطلاقه
public void PerformSpecialAttack()
{
    if (!attackPerformed && launched)
    {
        //اسمح بالهجوم الخاص مرة واحدة فقط
        attackPerformed = true;
        SendMessage("DoSpecialAttack", SendMessageOptions.DontRequireReceiver);
    }
}

//قم بسحب المقذوف إلى الموقع المحدد شريطة أن يكون قابلا للتحكم به من قبل اللاعب
public void Drag(Vector2 position)
{
    if (controllable && held && !launched)
    {
        transform.position = position;
    }
}

//تخبر ما إذا اللاعب يمسك حاليا بالمقذوف استعدادا لإطلاقه
public bool IsHeld()
{
    return held;
}

//تخبر ما إذا تم إطلاق المقذوف بالفعل
public bool IsLaunched()
{
    return launched;
}
}

لاحظ أن المتغير العام الوحيد في هذا البريمج هو lifeSpan والذي يحدد مدة بقاء المقذوف في المشهد بعد أن يتم إطلاقه. بخلاف ذلك لدبنا متغيرات الحالة وهي launched و held و controllable و attackPerformed، وكل هذه المتغيرات خاصة ولا يمكن التحكم بها إلا عن طريق إرسال الرسائل أو استدعاء الدّوال. إضافة لهذا المتغير العام لدينا أربع متغيرات خاصة تعبر عن الأوضاع المختلفة التي يكون فيها المقذوف منذ بداية اللعبة وحتى يتم إطلاقه إلى أن يختفي أخيرا من المشهد. هذه المتغيرات هي controllable و held و launched و attackPerformed وقيمتها المبدئية هي false. أوضاع المقذوف المختلفة تأتي على التسلسل التالي:

  • في بداية اللعبة يكون المقذوف موضوعا على الأرض بجانب مقلاع القذف، وفي هذه الأثناء يبقى ساكنا ولا يستطيع اللاعب التحكم به إلى أن يأتي دوره في الإطلاق. في هذه الحالة تكون قيمة المتغير controllable تساوي false، مما يمنع اللاعب من التحكم بالمقذوف.
  • بمجرد أن يحين دور المقذوف في الإطلاق ويتم وضعه على القاذف، يتم استدعاء الدّالة ()AllowControl، مما يغير قيمة controllable إلى true ويسمح للاعب بالتحكم بالمقذوف. حتى اللحظة تبقى المتغيرات الثلاث الأخرى على قيمتها false.
  • بمجرد أن يقوم اللاعب بالضغط بزر الفأرة على المقذوف، يتم استدعاء الدّالة ()Hold والتي يفترض أن تقوم بتغيير قيمة متغير الإمساك held إلى true. بما أن هذا المتغير يعبر عن أن اللاعب يمسك بالمقذوف، فإنّه ينبغي التأكد أولا من أنه مسموح للاعب بالتحكم به وذلك عن طريق فحص قيمة controllable، كما ينبغي التأكد من أنه غير ممسوك أصلا وذلك بفحص قيمة held نفسها، وأخيرا يجب فحص قيمة launched حتى يتم التأكد من أن المقذوف لم يتم إطلاقه بعد، وذلك لأنه لا يمكن إمساك المقذوف بعد إطلاقه. بعد التحقق من هذه الشروط الثلاث يتم تخزين الموقع الذي أمسك فيه اللاعب المقذوف في المتغير holdPosition ومن ثم يتم إرسال الرسالة ProjectileHeld للتبليغ بأن المقذوف تم إمساكه.
  • بعد الإمساك يبدأ اللاعب بتحريك المقذوف من أجل تجهيزه للإطلاق، حيث يقوم بسحبه للخلف والأسفل استعدادا لإطلاقه. أثناء إمساك اللاعب للمقذوف يُسمح له باستدعاء الدّالة ()Drag والتي يقوم من خلالها بتحريك المقذوف إلى موقع محدد. كما ترى فإنّ عملية التحريك عبر هذه الدّالة تعتمد على كون المقذوف قابلا للتحكم وممسوكا حاليا، إضافة إلى أنه يجب ألا يكون قد تم إطلاقه.
  • عندما يقوم اللاعب بإفلات المقذوف يتم استدعاء دالّة الإطلاق ()Launch والتي يمكن إعطاؤها قيمة رقمية تمثل معامل قوة الإطلاق. هذا المعامل يمكننا من عمل أكثر من مقلاع بقوى إطلاق مختلفة. عند استدعاء هذه الدّالة تقوم بالتحقق من كون المقذوف تحت تحكم اللاعب وأن اللاعب يمسكه حاليا، إضافة إلى أنه لم يتم إطلاقه حتى الآن. بعد تحقق هذه الشروط يتم حساب قوة الإطلاق عن طريق المسافة بين موقع إمساك المقذوف وموقع إفلاته وضربها بالمعامل المزوّد للدّالة، حيث أن شد المقذوف لمسافة أبعد سينتج عنه قوة إطلاق أكبر. بعدها يتم تفعيل الجسم الصلب مرة أخرى عن طريق ضبط المتغير isKinematic إلى false وبالتالي عودة تنشيط استجابة الجسم الصلب للقوى الخارجية قبل إضافة قوة الإطلاق له. بعد تنفيذ الإطلاق يتم تحديث متغيرات الحالة؛ حيث يمنع اللاعب من التحكم بالمقذوف عن طريق تغيير controllable إلى false ويتم تنشيط حالة الإطلاق عن طريق تغيير launched إلى true ويتم أيضا إعادة held إلى false حيث أن اللاعب لم يعد ممسكا بالمقذوف. أخيرا يتم إرسال الرسالة ProjectileLaunched من أجل إبلاغ البريمجات الأخرى بأن المقذوف قد تم إطلاقه.
  • بعد اطلاق المقذوف بقي هناك خطوة أخيرة يمكن للاعب القيام بها بخصوصه، وهي تنفيذ الهجوم الخاص بالمقذوف؛ كالانشطار إلى 3 مقذوفات أصغر أو مضاعفة السرعة أو غيرها. يمكن للاعب تنفيذ هذا الهجوم عن طريق استدعاء الدّالة ()PerformSpecialAttack والتي تتأكد من أن قيمة attackPerformed هي false؛ حيث أن تنفيذ هذا الهجوم مسموح مرة واحدة فقط. إضافة لهذا الشرط يجب التأكد من أن المقذوف تم إطلاقه بالفعل عن طريق فحص المتغير launched؛ ذلك أن هذا الهجوم يمكن تنفيذه فقط بعد إطلاق المقذوف. كما تلاحظ فإنّ هذه الدّالة لا تقوم بتنفيذ الهجوم فعليا، عوضا عن ذلك تقوم بإرسال الرسالة DoSpecialAttack والتي سيقوم بريمج آخر باستقبالها وتنفيذ الهجوم الفعلي بناء عليها. بفصل استدعاء الهجوم عن تنفيذه نواصل عملنا على مبدأ فصل الاهتمامات ونمكّن أنفسنا من برمجة أكثر من نوع هجوم دون أن يؤثر ذلك على هيكل البريمج الأساسي.

بخلاف هذه المراحل نلاحظ وجود الدّالتين ()IsHeld و ()IsLaunched واللتين تمكنان البريمجات الأخرى من قراءة قيم المتغيرات الخاصة ولكن دون تغيير قيمتها. قراءة هذين المتغيرين ستكون ذات أهمية للبريمجات التي يعتمد عملها على بريمج المقذوف كما سنرى بعد قليل. ملاحظة أخرى هي استخدام الخيار SendMessageOptions.DontRequireReceiver عند إرسال الرسالة DoSpecialAttack وبذلك لا نشترط وجود مستقبل للرسالة. السبب هو أن هذا الهجوم اختياري ولا بأس من وجود مقذوفات ليس لها أي هجوم خاص.

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

البريمج الأول هو ProjectileSounds وهو المسؤول عن أصوات المقذوفات. ما يفعله هذا البريمج ببساطة هو استقبال رسالتي الإمساك ProjectileHeld والإطلاق ProjectileLaunched وتشغيل الملف الصوتي المحدد لكل عملية. السرد التالي يوضح هذا البريمج:

using UnityEngine;
using System.Collections;
 
public class ProjectileSounds : MonoBehaviour {
 
      //الملف الصوتي الخاص بعملية الإطلاق
      public AudioClip launchSound;
 
      //الملف الصوتي الخاص بعملية الإمساك
      public AudioClip holdSound;
 
 	   //تستدعى مرة واحدة عند بداية التشغيل
 	   void Start () {
 	
 	   }
 	
 	   //تستدعى مرة عند تصيير كل إطار
 	   void Update () {
 	    
 	   }
 
     void ProjectileHeld()
     {
         AudioSource.PlayClipAtPoint(holdSound, transform.position);
     }
 
     void ProjectileLaunched()
     {
         AudioSource.PlayClipAtPoint(launchSound, transform.position);
     }
 }

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

1.thumb.png.83b7b39916f98d3d4562f97168a6

الآن أصبح بإمكاننا كتابة بريمج رسم المسار وإضافته لقالب المقذوف. هذا البريمج موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class PathDrawer : MonoBehaviour {

    //القالب المستخدم لرسم النقاط
    public GameObject pathPointPrefab;

    //المسافة بين كل نقطتين متتابعتين
    public float pointDistance = 0.75f;

    //الكائن الأب لكائنات نقاط المسار
    Transform pathParent;

    //متغير لتخزين موقع آخر نقطة تمت إضافتها
    Vector2 lastPointPosition;

    //متغير داخلي لمعرفة ما إذا كان المقذوف قد تم إطلاقه أم لا
    bool launched = false;

	   //تستدعى مرة واحدة عند بداية التشغيل
	   void Start () {
	       //Path ابحث عن الكائن الأب لنقاط المسار والمسمى
        pathParent = GameObject.Find("Path").transform;
	   }
	
	   //تستدعى مرة عند تصيير كل إطار
	   void Update () {
        if (launched)
        {
            float dist = Vector2.Distance(transform.position, lastPointPosition);
            if (dist >= pointDistance)
            {
                //حان الوقت للإضافة نقطة جديدة
                AddPathPoint();
            }
        }
	   }

    void ProjectileLaunched()
    {
        //تم إطلاق المقذوف للتو لذا احذف المسار السابق
        for (int i = 0; i < pathParent.childCount; i++)
        {
            Destroy(pathParent.GetChild(i).gameObject);
        }

        AddPathPoint();

        //قم بتحديث قيمة المتغير حيث أن المقذوف قد تم إطلاقه
        launched = true;
    }

    //تقوم بإضافة نقطة جديدة للمسار
    void AddPathPoint()
    {
        //قم بإنشاء نقطة جديدة مستخدما القالب
        GameObject newPoint = (GameObject)Instantiate(pathPointPrefab);
        //قم بوضع النقطة في الموقع الحالي للمقذوف
        newPoint.transform.position = transform.position;
        //قم بضبط الكائن الأب للنقطة
        newPoint.transform.parent = pathParent;
        //قم بتخزين موقع النقطة التي تمت إضافتها
        lastPointPosition = transform.position;
    }
}

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

تذكر أنه عند إطلاق المقذوف سيقوم البريمج Projectile بإرسال الرسالة ProjectileLaunched، والتي يستقبلها البريمج PathDrawer عن طريق الدّالة التي تحمل نفس الاسم. بمجرد وصول الرسالة يتم حذف المسار المرسوم سابقا (إن وُجد) وذلك عن طريق حذف جميع أبناء الكائن الفارغ Path والذي نحتفظ بمرجع له في المتغير pathParent. بعد الانتهاء من الحذف نرسم نقطة في موقع الإطلاق عن طريق استدعاء الدّالة ()AddPathPoint ومن ثم يتم تغيير قيمة launched إلى true.

ما تقوم به الدّالة ()AddPathPoint هو إنشاء نقطة جديدة في الموقع الحالي للمقذوف باستخدام القالب pathPointPrefab وإضافتها كابن للكائن Path ومن ثم تخزين موقعها في المتغير lastPointPosition. طالما أن كائن المقذوف موجود في المشهد سيتم استدعاء الدّالة ()Update في كل إطار، إلا أنها لن تقوم بأي شيء إلى أن تتغير قيمة launched إلى true. إذا تحقق هذا الشرط فهذا يعني أن المقذوف قد تم إطلاقه وبالتالي يجب أن يتم رسم المسار أثناء حركته؛ لذلك نقوم بحساب المسافة بين موقع المقذوف الحالي transform.position وموقع رسم النقطة السابقة lastPointPosition. إذا زادت هذه المسافة أو تساوت مع pointDistance فإن الوقت يكون قد حان لإضافة نقطة جديدة لهذا يتم استدعاء ()AddNewPoint. الصورة التالية تمثل عملية رسم مسار المقذوف أثناء حركته:

2.thumb.png.97a68dafa843fccded186c53ddcb

الهجمات الخاصة للمقذوفات

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

لنبدأ مع الهجوم الأسهل وهو هجوم السرعة. نظرا لكون منطق الهجمات يختلف تماما بين هجوم وآخر، علينا أن نفصل كل هجوم في بريمج منفصل. العامل المشترك الوحيد بين هذه الهجمات هو أنها ستستقبل الرسالة DoSpecialAttack والتي يرسلها بريمج المقذوف Projectile عندما يتم استدعاء الدّالة ()PerformSpecialAttack ويتم التحقق من الشروط اللازمة لتنفيذ هذا الهجوم. بريمج تنفيذ هجوم السرعة يسمى SpeedAttack، وما يفعله هو جلب مكوّن الجسم الصلب ومن ثم مضاعفة سرعته بمقدار محدد دون تغيير اتجاهها. هذا البريمج موضح في السرد التالي. تذكر أن بريمجات الهجمات يجب أن تتم إضافتها إلى قوالب المقذوفات.

using UnityEngine;
using System.Collections;

public class SpeedAttack : MonoBehaviour {

     //قم بضرب السرعة الحالية للمقذوف بهذا
     //المقدار عند تنفيذ الهجوم
     public float speedFactor = 1.5f;

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

    //وبناء عليها تنفّذ هجوم السرعة DoSpecialAttack تقوم باستقبال الرسالة
    public void DoSpecialAttack()
    {
        //اجلب مكوّن الجسم الصلب لكائن المقذوف
        Rigidbody2D myRB = GetComponent<Rigidbody2D>();

        //قم بضرب السرعة بمقدار المضاعفة ومن ثم اضبط سرعة الكائن على الناتج الجديد
        myRB.velocity = myRB.velocity * speedFactor;
    }
}

النوع الثاني من الهجمات الخاصة كما ذكرنا هو الهجوم الانشطاري، والذي يؤدي إلى تفتت المقذوف إلى مقذوفات أصغر حجما (شظايا)، والتي بدورها تتناثر على مساحة واسعة نسبيا. قبل الانتقال إلى البريمج الخاص بهذا الهجوم، نلاحظ أن تنفيذه سيحتاج لإنشاء كائنات جديدة وهي الشظايا التي ستتناثر جراء تنفيذ الهجوم. معنى هذا أننا سنحتاج لبناء قوالب لهذه الشظايا، وسيكون هناك قالبان تحديدا: واحد لشظايا مقذوف الفيل والآخر لشظايا مقذوف الزرافة. سأسمي هذين القالبين ElephantCluster و GiraffeCluster، وهما فعليا يتشابهان في كل شيء باستثناء الصورة المعروضة. هذان القالبان بسيطان حيث يحمل كل منهما صورة المقذوف الأصلي مع تصغير قياس الكائن إلى 0.75 على المحورين x و y وذلك لجعل الشظايا أصغر من المقذوف الأصلي. إضافة لذلك سنضيف مكوّن جسم صلب Rigid Body 2D و مكوّن تصادم Circle Collider 2D، وبهذا يصبح قالبا الشظايا جاهزين.

البريمج الذي سيطبق هذا النوع من الهجوم يسمى ClusterAttack وهو موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class ClusterAttack : MonoBehaviour {

    //القالب الذي سيستخدم لإنشاء الشظايا
    public GameObject clusterPrefab;

     //عدد الثواني التي ستعيشها كل شظية قبل تدميرها وحذفها من المشهد
    public float clusterLife = 4.0f;

    //كم عدد الشظايا التي ستنتج من هذا المقذوف
    public int clusterCount = 3;

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

    //وبناء عليها تنفذ الهجوم الانشطاري DoSpecialAttack تعمل على استقبال الرسالة
    public void DoSpecialAttack()
    {
        //اجلب السرعة الحالية للمقذوف الأصلي
        Rigidbody2D myRB = GetComponent<Rigidbody2D>();
        float originalVelocity = myRB.velocity.magnitude;

        //قم بتخزين جميع مكوّنات التصادم الخاصة بالشظايا في هذه المصفوفة
        Collider2D[] colliders = new Collider2D[clusterCount];
        Collider2D myCollider = GetComponent<Collider2D>();

        for (int i = 0; i < clusterCount; i++)
        {
            //قم بإنشاء شظية جديدة
            GameObject cluster = (GameObject)Instantiate(clusterPrefab);
            //قم بضبط الموقع والاسم والأب لكائن الشظية
            cluster.transform.parent = transform.parent;
            cluster.name = name + "_cluster_" + i;
            cluster.transform.position = transform.position;
            //قم بتخزين مكوّن تصادم الشظية في الموقع الحالي في المصفوفة
            colliders[i] = cluster.GetComponent<Collider2D>();
            //أهمل التصادم الذي يمكن أن يحص بين هذه الشظية والشظايا التي تم إنشاؤها قبلها
            //إضافة إلى التصادم الذي يمكن أن يقع بين الشظية والكائن الأصلي
            Physics2D.IgnoreCollision(colliders[i], myCollider);
            for (int a = 0; a < i; a++)
            {
               Physics2D.IgnoreCollision(colliders[i], colliders[a]);
            }
            
            Vector2 clusterVelocity;
            //مع كل شظية جديدة نقوم بتقليل مركبة السرعة أفقيا وزيادتها عموديا من أجل ضمان تشتت الشظايا
            clusterVelocity.x = (originalVelocity / clusterCount) * (clusterCount - i);
            clusterVelocity.y = (originalVelocity / clusterCount) * -i;
            
            //اجلب كائن الجسم الصلب للشظية الجديدة
            Rigidbody2D clusterRB = cluster.GetComponent<Rigidbody2D>();
            clusterRB.velocity = clusterVelocity;
            //قم بتحديد كتلة الشظية لتساوي كتلة الكائن الأصلي
            clusterRB.mass = myRB.mass;
            //قم بتدمير الشظية بعد انقضاء العمر المحدد لها
            Destroy(cluster, clusterLife);
        }

        //قم أخيرا بتدمير كائن المقذوف الأصلي
        Destroy(gameObject);
    }
}

فكرة عمل هذا الهجوم تقوم على استقبال الرسالة DoSpecialAttack ومن ثم إنشاء العدد المحدد من الشظايا باستخدام القالب المحدد لها. من أجل منع التصادم بين الشظايا وبعضها وأيضا بين الشظايا والمقذوف الأصلي - حيث من الممكن حدوث تصادم في اللحظة التي تسبق حذفه من المشهد – نقوم باستخدام الدّالة ()Physics2D.IgnoreCollision ونزودها بمكوّني التصادم اللذين نرغب بإهمال التصادمات بينها. لاحظ أننا عرفنا مصفوفة من مكوّنات التصادم من أجل تخزين مكوّنات جميع الشظايا، وعند إنشاء شظية جديدة نمر على مكوّنات تصادم الشظايا السابقة ونستدعي الدّالة المذكورة بين المكوّنين القديم والجديد من أجل إهمال التصادمات. الخطوة التالية تتعلق بسرعة حركة الشظية، حيث نأخذ مقدار سرعة المقذوف الأصلي ونضربه كل مرة بقيمة مختلفة لنحصل على المركّبتين الأفقية والعمودية للسرعة الجديدة. هاتان المركبتان تتغيران من شظية لأخرى، حيث تبدأ الشظية الأولى بمركبة أفقية عالية وعمودية منخفضة، ومن ثم تبدأ هذه القيم بالتغير حيث تزداد القيمة العمودية نحو الأسفل تدريجيا وتقل الأفقية، مما ينتج عنه تشتت المقذوفات بطريقة تشبه ما تراه في الصورة أدناه (قمت في هذه الصورة بزيادة عدد الشظايا لتوضيح الفكرة):

3.thumb.png.f97547a34356979121c07a05c10f

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

صناعة قاذف الإطلاق

لننتقل الآن إلى القاذف وهو المقلاع الذي سيطلق هذه المقذوفات نحو أهدافها. للأسف فإن حزمة الرسومات التي بين أيدينا لا تحتوي على صورة مقلاع، لذا سنحاول استخدام بعض الأشكال الخشبية والمعدنية لصنع شكل بسيط يشبهه. سأستخدم هنا 3 مستطيلات خشبية ومثلثا حجريا لصنع الشكل الذي تراه في الصورة التالية. هذه الكائنات يجب أن توضع كأبناء لكائن فارغ واحد يحتويها جميعا، ويجب أيضا تعديل قيمة Order in Layer في مكوّن Sprite Renderer الخاص بالمثلث وجعلها 1، وذلك حتى يظهر أمام القطع الخشبية كما هو موضح في الصورة:

4.thumb.png.72ea5a95f8b65375d3391af49176

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

أول هذه البريمجات هو البريمج الرئيسي الذي يعمل على إطلاق المقذوفات على الأهداف. لنتعرف على هذا البريمج المسمى Launcher في السرد التالي ومن ثم نناقش تفاصيل وظائفه:

using UnityEngine;
using System.Collections;

public class Launcher : MonoBehaviour {

     //معامل قوة الإطلاق الخاصة بهذا القاذف
     public float launchForce = 1.0f;
    
     //أقصى طول يمكن لحبل القاذف المطاطي أن يمتد إليه
    public float maxStretch = 1.0f;

    //الموقع الذي سيتم وضع المقذوف الحالي فيه قبل أن يمسكه اللاعب
    public Transform launchPosition;

    //المقذوف الحالي الموضوع على القاذف
    public Projectile currentProjectile;

    //هل تم إطلاق جميع المقذوفات الموجودة في المشهد؟
    private bool projectilesConsumed = false;

	   //تستدعى مر واحدة عند بداية التشغيل
	   void Start () {

	   }
	
	   //تستدعى مرة عند تصيير كل إطار
	   void Update () {
        if (projectilesConsumed)
        {
            //لا يوجد ما يمكن فعله
            return;
        }

        if (currentProjectile != null)
        {
		    //إن لم يكن المقذوف الحال قد تم إطلاقه ولم يتم أيضا إمساكه من قبل اللاعب
		    //فقم حينها بجلب المقذوف إلى الموقع المخصص للإطلاق
            if (!currentProjectile.IsHeld() && !currentProjectile.IsLaunched())
            {
                BringCurrentProjectile();
            }
        }
        else
        {
		    //لا يوجد أي مقذوف حاليا على القاذف
		    //قم بالبحث عن أقرب مقذوف وإحضاره لموقع الإطلاق
            currentProjectile = GetNearestProjectile();
            if (currentProjectile == null)
            {
			//تم استهلاك كل المقذوفات، أرسل رسالة تخبر بذلك
                projectilesConsumed = true;
                SendMessageUpwards("ProjectilesConsumed");
            }
        }
	   }

    //تقوم بالبحث عن أقرب مقذوف للقاذف وإرجاعه
    Projectile GetNearestProjectile()
    {
        Projectile[] allProjectiles = FindObjectsOfType<Projectile>();
        
        if (allProjectiles.Length == 0)
        {
            //لم يعد هناك أي مقذوفات
            return null;
        }

        //قم بالبحث عن أقرب مقذوف وإرجاعه
        Projectile nearest = allProjectiles[0];
        float minDist = Vector2.Distance(nearest.transform.position, transform.position);

        for (int i = 1; i < allProjectiles.Length; i++)
        {
         float dist = Vector2.Distance(allProjectiles[i].transform.position, transform.position);
            if (dist < minDist)
            {
                minDist = dist;
                nearest = allProjectiles[i];
            }
        }

        return nearest;
    }

    //تقوم بتحريك المقذوف الحالي خطوة واحدة وبشكل سلس نحو موقع الإطلاق
    void BringCurrentProjectile()
    {
        //اجلب المواقع التي سيتحرك المقذوف بينها
        Vector2 projectilePos = currentProjectile.transform.position;
        Vector2 launcherPos = launchPosition.transform.position;

        if (projectilePos == launcherPos)
        {
            //المقذوف في موقع الإطلاق فعلا، لا داعي لتحريكه
            return;
        }
	       //استخدم الاستيفاء الخطي مع الوقت المنقضي بين الإطارات من أجل الحركة السلسة
        projectilePos = Vector2.Lerp(projectilePos, launcherPos, Time.deltaTime * 5.0f);
        //ضع المقذوف في موقعه الجديد
        currentProjectile.transform.position = projectilePos;

        if (Vector2.Distance(launcherPos, projectilePos) < 0.1f)
        {
	     //المقذوف أصبح قريبا جدا، ضعه مباشرة في موقع الإطلاق
            currentProjectile.transform.position = launcherPos;
            currentProjectile.AllowControl();
        }
    }

    //تقوم بإمساك المقذوف الحالي
    public void HoldProjectile()
    {
        if (currentProjectile != null)
        {
            currentProjectile.Hold();
        }
    }

    //تقوم بسحب المقذوف الحالي لموقع جديد
    public void DragProjectile(Vector2 newPosition)
    {
        
        if (currentProjectile != null)
        {
            //تأكد من عدم تجاوز الحد الأقصى لشد الحبل المطاطي
            float currentDist = Vector2.Distance(newPosition, launchPosition.position);
            
            if (currentDist > maxStretch)
            {
                //قم بتغيير الموقع المزود إلى أبعد نقطة مسموح بها على امتداده
                float lerpAmount = maxStretch / currentDist;
                newPosition = Vector2.Lerp(launchPosition.position, newPosition, lerpAmount);
            }

            //ضع المقذوف في الموقع الجديد
            currentProjectile.Drag(newPosition);
        }
                
    }

    //تقوم بإفلات المقذوف الحالي وإطلاقه إذا كان اللاعب يمسكه
    public void ReleaseProjectile()
    {
        if (currentProjectile != null)
        {
            currentProjectile.Launch(launchForce);
        }
    }
}

المتغيرات العامّة في هذا البريمج هي launchForce والذي يمثل قوة الإطلاق و maxStretch وهو أقصى مسافة مسموح بها بين المقذوف وموقع الإطلاق أثناء الشد (أي أقصى امتداد للخيط المطاطي) و launchPosition وهو متغير لتخزين كائن موقع الإطلاق المسمى LaunchPos الذي سبق وأضفناه لقالب المقلاع حين قمنا بإنشائه. أخيرا لدينا مرجع للمقذوف الحالي الموجود على القاذف وهو currentProjectile.

في كل دورة تحديث تقوم الدّالة ()Update بفحص ما إذا كان هناك مقذوف حالي أم لا، وفي حالة عدم وجوده فإنها تقوم باستدعاء الدّالة ()GetNearestProjectile والتي تبحث عن أقرب المقذوفات للقاذف وتقوم بإرجاعه. إذا لم تجد هذه الدّالة أية مقذوفات في المشهد فإنها تعيد القيمة null وفي هذه الحالة يقوم البريمج بإرسال الرسالة ProjectilesConsumed نحو أعلى هرمية المشهد من أجل إبلاغ البريمجات التي تتحكم بحالة اللعبة بأن اللاعب قد استنفد جميع مقذوفاته في هذه المرحلة. حين استنفاد جميع المقذوفات يتم تغيير قيمة المتغير projectilesConsumed إلى true مما يعني أنّ الدّالة ()Update لن تقوم بعمل أي شيء بعد الآن. أمّا في حالة وجود مقذوف حالي لم يقم اللاعب بإمساكه أو إطلاقه بعد، فإن ()Update تقوم باستدعاء الدّالة ()BringCurrentProjectile والتي تعمل على تحريك المقذوف الحالي نحو موقع الإطلاق بشكل سلس (تذكر أن الموقع الأصلي للمقذوفات هو على الأرض بجانب المقلاع). عند وصول المقذوف للموقع launchPosition فإنّ هذه الدّالة ستتوقف تلقائيا عن تحريكه حتى لو استمر استدعاؤها من قبل ()Update.

سأتحدث هنا بقليل من التفصل عن الدّالة ()BringCurrentProjectile حتى أشرح الآلية التي تستخدمها من أجل تحقيق الحركة السلسة للمقذوف من موقعه الحالي باتجاه موقع الإطلاق. الحركة السلسة في محركات الألعاب تعتمد على عامل الزمن المنقضي بين كل إطارين متتابعين، وهو في Unity المتغير Time.deltaTime. إضافة لهذا المتغير سنحتاج لدّالة تحسب الاستيفاء الخطي Linear Interpolation بين قيمتين مختلفتين. لكن ما هو الاستيفاء الخطي؟ هو ببساطة عبارة عن قيمة محصورة بين حدين أسفل وأعلى. هذه القيمة قد تكون رقما مجردا بين رقمين، أو موقعا بين موقعين، أو لونا بين لونين، إلخ. لكن أين تقع هذه القيمة بالتحديد بين الحدين؟ ما يحدد هذا الموقع هو قيمة الاستيفاء، وهي قيمة كسرية بين الصفر والواحد، فكلما زادت القيمة كان الناتج أقرب للحد الأعلى، وكلما قلت كان الناتج أقرب للحد الأدنى وهكذا. فمثلا لو أردنا حساب الاستيفاء بين العددين صفر و 10، وكانت قيمة الاستيفاء هي 0.6، فإن الناتج سيكون العدد 6، وإذا كانت قيمة الاستيفاء 0.45 فإنّ الناتج سيكون العدد 4.5 وهكذا.

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

الدّوال الثلاث ()HoldProjectile و ()DragProjectile و ()ReleaseProjectile تقوم باستدعاء دوال الإمساك ()Hold و التحريك ()Drag و الإطلاق ()Launch للمقذوف الحالي currentProjectile. الدّالة التي تحتاج لبعض الشرح هنا هي ()DragProjectile وذلك لأنها تحتوي على خطوة إضافية غير موجودة في البريمج Projectile، ألا وهي التحقق من كون بعد المقذوف عن موقع الإطلاق الأولي أثناء سحبه للخلف لا يتجاوز الطول المسموح به لتمدد الحبل المطاطي للمقلاع. هذا التمدد معرّف في المتغير maxStretch. الطريقة التي سنعتمدها لفرض هذا الحد الأقصى للطول يجب أن تراعي سهولة التحكم أيضا، فلو سحب اللاعب المؤشر أبعد من الطول المسموح يجب ألا ينسحب معه المقذوف، إلا أنه في نفس الوقت يجب أن يبقى قادرا على تغيير زاوية الإطلاق. من أجل تحقيق هذه الآلية سنستخدم الاستيفاء الخطي مرة أخرى وهذا الاستخدام موضح في السطرين 131 و 132.

الفكرة هي أن نحسب المسافة بين موقع الإطلاق والموقع الحالي للمؤشر currentDist ومقارنته بالحد الأقصى للتمدد وهو maxStretch. فإذا تجاوزت هذه المسافة الحد المسموح، سنقوم بقسمة maxStretch على currentDist، وبالتالي نحصل على نسبة الاستيفاء اللازمة بين موقع الإطلاق الأصلي launchPosition.position و الموقع الحالي للمؤشر newPosition. بطبيعة الحال ستقل هذه القيمة بزيادة بعد المؤشر عن موقع الإطلاق، وبالتالي تحافظ على مسافة ثابتة عن موقع الإطلاق وهي المسافة التي تساوي maxStretch. وبتنفيذ الاستيفاء نحصل على الموقع الجديد الصحيح newPosition دون أن نؤثر على سلاسة الحركة، ومن ثم نستخدم الدّالة ()Drag لتحريك المقذوف. من الضروري استخدام هذه الدّالة وليس تحريك المقذوف مباشرة وذلك لأنها تتحقق من الشروط من حيث كون المقذوف ممسوكا من قبل اللاعب ولم يتم إطلاقه، وهي شروط الحركة حسب قواعد اللعبة.

بعد الانتهاء من كتابة البريمج علينا أن نضيفه إلى الكائن الفارغ Launcher وهو الجذر لكل كائنات القطع التي يتكون منها المقلاع. البريمج التالي الذي سنضيفه سيقوم برسم الحبل المطاطي بين طرفي المقلاع والمقذوف. لكن قبل الانتقال للبريمج علينا أن نضيف المكوّن المسؤول عن رسم الخط الذي سيمثل هذا الحبل. المكوّن الذي سنضيفه يسمى Line Renderer ويمكن إضافته كالعادة من الزر Add Component ومن ثم كتابة اسم المكوّن كما في الصورة التالية. يقوم هذا المكوّن برسم خط متصل بين مجموعة من النقاط المحددة له عبر المصفوفة positions مبتدئا بأول نقطة في المصفوفة إلى آخر نقطة:

5.thumb.png.b98b41da7f54df036bf610ff2af7

بعد إضافة المكوّن علينا أن نعدل بعض قيمه: فأولا علينا تغيير عدد النقاط التي ترسم الخط positions إلى 3 ومن ثم نجعله أقل سماكة عن طريق تغيير كل من Start Width و End Width إلى 0.1، وأخيرا سنقوم بتغيير لونه للأحمر عند بدايته ونهايته (يمكنك بالطبع اختيار أي لون آخر). هذه الإعدادات موضحة في الصورة التالية:

6.thumb.png.84e116f8753aabc20d1293209ed4

لننتقل الآن للبريمج LauncherRope وهو المسؤول عن رسم هذا الخط بين طرفي المقلاع والمقذوف. هذا البريمج موضح في السرد التالي:

using UnityEngine;
using System.Collections;

public class LauncherRope : MonoBehaviour {

     //موقع الطرف الأيسر للحبل
     public Transform leftEnd;

     //موقع الطرف الأيمن للحبل
    public Transform rightEnd;

    //مرجع لبريمج القاذف
    Launcher launcher;

    //مرجع لمكوّن تصيير الخط المضاف للكائن
    LineRenderer line;

	   //تستدعى مرة عند بداية التشغيل
	   void Start () {
        launcher = GetComponent<Launcher>();
        line = GetComponent<LineRenderer>();
	       //قم بإخفاء الخط في البداية بتعطيل مكوّنه
        line.enabled = false;
	   }
	
	   //تستدعى مرة عند تصيير كل إطار
	   void Update () {
        //أظهر الخط فقط في حال كان المقذوف ممسوكا من قبل اللاعب
        if (launcher.currentProjectile != null &&
            launcher.currentProjectile.IsHeld())
        {
            if (!line.enabled)
            {
                line.enabled = true;
            }
            //قم برسم الخط ابتداء من الطرف الأيسر فالمقذوف فالطرف الأيمن
            line.SetPosition(0, leftEnd.position);
            line.SetPosition(1, launcher.currentProjectile.transform.position);
            line.SetPosition(2, rightEnd.position);
        }
        else
        {
            line.enabled = false;
        }
	   }
}

يمكنك أن تلاحظ مدى بساطة هذا البريمج، فكل ما يقوم به هو تعطيل مكوّن رسم الخط في البداية، ومن ثم يقوم بفحص حالة المقذوف الحالي (إن وُجد). في حال كان هذا المقذوف ممسوكا من قبل اللاعب يتم تفعيل المكوّن LineRenderer مما يجعل الخط مرئيا، ومن ثم يقوم بضبط مواقع رسم الخط. تذكر الكائنين الفارغين اللذين أضفناهما كإبنين للمقلاع وهما RightEnd و LeftEnd. سنستخدم المرجعين leftEnd و rightEnd المعرّفين في البريمج ونربطهما عن طريق المستعرض بهذين الكائنين. بالتالي فإننا نكون قد حددنا موقع النقطة الأولى والأخيرة للخط المرسوم. بقي أن نحدد موقع النقطة الوسطى وهي بطبيعة الحال موقع المقذوف. لاحظ أننا نستخدم الدّالة ()SetPosition ونعطيها ترتيب الموقع في المصفوفة متبوعا بالنقطة التي نريد أن يكون فيها هذا الموقع. عند تشغيل اللعبة وإمساك المقذوف سيظهر هذا الخط بالشكل التالي:

7.thumb.png.2f4a42754b44c612b1ce10142ffa

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

using UnityEngine;
using System.Collections;

public class LauncherMouseInput : MonoBehaviour {

     //مرجع لبريمج الإطلاق
     private Launcher launcher;

	   //تستدعى مرة واحدة عند بداية التشغيل
	   void Start () {
        launcher = GetComponent<Launcher>();
	   }
	
	   //تستدعى مرة عند تصيير كل إطار
	   void Update () {
        CheckButtonDown();
        CheckDragging();
        CheckButtonUp();
	   }

    void CheckButtonDown()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //تم ضغط زر الفأرة الأيسر للتو
            //هل يوجد مقذوف حالي؟
            if (launcher.currentProjectile != null)
            {
                //قم بتحويل موقع المؤشر من إحداثيات الشاشة إلى إحداثيات فضاء المشهد
                Vector2 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                //قم باستخراج مكوّن التصادم من الكائن
                Collider2D projectileCol = launcher.currentProjectile.GetComponent<Collider2D>();
                //هل يقع مؤشر الفأرة ضمن حدود مكوّن التصادم الخاص بالمقذوف؟
                if (projectileCol.bounds.Contains(mouseWorldPos))
                {
                    //نعم، أي أنه تم ضغط زر الفأرة فوق المقذوف
                    //قم بإمساك المقذوف
                    launcher.HoldProjectile();
                }
            }
        }
    }

    //تقوم بفحص ما إذا كان اللاعب يسحب الفأرة مستخدما الزر الأيسر
    void CheckDragging()
    {
        if (Input.GetMouseButton(0))
        {
            Vector2 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            launcher.DragProjectile(mouseWorldPos);
        }
    }

    //تفحص ما إذا كان قد تم رفع الضغط عن زر الفأرة الأيسر
    void CheckButtonUp()
    {
        if (Input.GetMouseButtonUp(0))
        {
            //تم رفع الضغط عن الزر الأيسر
            //قم بإطلاق المقذوف
            launcher.ReleaseProjectile();
        }
    }
}

تقوم دالّة التحديث ()Update في هذا البريمج باستدعاء ثلاث دوال أخرى على الترتيب وهي ()CheckButtonDown ثم ()CheckDragging ثم ()CheckButtonUp. في الدّالة ()CheckButtonDown يتم التأكد أولا من وجود مقذوف على المقلاع، فإن وُجد هذا المقذوف يتم تحويل موقع مؤشر الفأرة من إحداثيات الشاشة إلى إحداثيات المشهد، ومن ثم فحص ما إذا كان هذا الموقع يقع ضمن حدود مكوّن التصادم الخاص بالمقذوف. تحقق هذا الشرط معناه أن اللاعب قد ضغط بزر الفأرة الأيسر على المقذوف وبالتالي فإنه أمسكه، لذا يتم استدعاء الدّالة ()Hold من البريمج Launcher. في الدّالة ()CheckDragging يتم التحقق من بقاء زر الفأرة الأيسر مضغوطا، وفي هذه الحالة يتم تحريك المقذوف إلى موقع المؤشر عبر استدعاء الدّالة ()DragProjectile. تذكر أنّ هذه الدّالة تمنع تجاوز المقذوف للحد الأقصى لتمدد الحبل المطاطي، بالتالي وبغض النظر عن موقع المؤشر سيبقى المقذوف ضمن هذا الحد. أخيرا فإنّ الدّالة ()CheckButtonUp تقوم بفحص ما إذا أفلت اللاعب زر الفأرة الأيسر، وفي هذه الحالة تستدعي الدّالة ()ReleaseProjectile من بريمج القاذف حتى يتم إطلاق المقذوف. الشكل التالي يمثل المكوّنات النهائية لقالب مقلاع الإطلاق:

8.thumb.png.fc8ab33ea296b12138c5f185f296

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

9.thumb.png.53867a52b3ba4577b32c0f54dbcc





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


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن