السلام عليكم ورحمة الله
قمنا في المقالات السابقة من هذه السلسلة بتحريك الشخصية كجسم صلب Rigidbody عن طريق الفيزياء.
واستجبنا لأوامر اللاعب مثل المشي والقفز.
والآن سنجعل اللعبة أكثر حيوية – سنضيف شخصية جميلة ونجعلها “تتحرك” بناءً على وضعها الحالي.
وسنعتمد على مورد جاهز للحصول على الشخصية، من Kenny Assets، وهذه هي النتيجة التي سنحصل عليها:
المحتوى
- تحميل الموارد
- ما هو الأسلوب الذي سنتبعه للتحريك؟
- إضافة Animator و Animation Clip
- تغيير الصورة للوضعية الثابتة في الملف Idle
- إنشاء ملف Animation Clip للمشي Walk
- استخدام الـAnimator والـAnimator Controller
- الربط بين الوقوف والمشي – إضافة معطيات Parameters
- إنشاء ملف برمجي خاص بالحركيات للشخصية
- موازنة سرعة تقليب الصور بسرعة الشخصية
- إنشاء ملف Animation للقفز
- إنشاء ملف Animation للهبوط
- مشكلة تكرار إشارة Jumped
ماذا سنتعلم؟
- إنشاء ملفات حركية Animations
- إنشاء Animator Controller وإضافة الحالات له
- استعمال الـTransitions للانتقال من حالة لأخرى
- التعامل مع الـAnimator من الملف البرمجي لمزامنة الحركيات مع الحركة الفيزيائية.
ملاحظة:
هذه المقالة يعتمد على مقالات سابقة قمنا بها ببرمجة الحركات والميكانيكيات الأساسية، لذا راجعوهم إذا واجهتم مشكلة ما أولاً.
ولكن يمكن قراءة هذه المقالة والاستفادة منه دون الحاجة لمتابعة هذه السلسلة كلها.
تحميل الموارد
أول خطوة نفعلها هي الحصول على الموارد، كما ذكرت سنستخدم ملفات Kenney، بالتحديد حزمة الـPlatformer:
https://www.kenney.nl/assets/platformer-art-deluxe
هذه الحزمة مجانية ويمكن استخدامها للمشاريع التجارية، نشكر المطور والمصمم Kenney على توفيرها بهذا الأسلوب.
قوموا بتحميلها من الرابط أعلاه. الحزمة تحتوي على العديد من الصور، منها صور للشخصيات وللبيئة وللعناصر وما إلى ذلك.
ما يهمنا الآن هو أحد الشخصيات المتوفرة، هناك خمسة بألوان مختلفة، تأتي على مجموعة صور. وكلها مماثلة في وضعياتها.
اختاروا ما يعجبكم منها، بالنسبة لي سأختار الشخصية alienGreen.
ولكن لمتابعة هذا المقال، يجب الاختيار بين الأزرق والأخضر والزهري، لاحتواء الحزمة على صور حركات مشي إضافية لها سنستخدمها.
بعد تنزيلها ذهبت إلى مشروعنا في محرر الـUnity أولاً، وأنشأت ملفًا جديدًا داخل المجلد Sources، سميته Platformer Pack، وبداخله أيضًا أنشأت مجلدًا آخر سميته Player Sprites.
ثم ذهبت إلى مجلدات الحزمة التي نزلناها وأحضرت الصور الآتية من المجلد:
Extra animations and enemies -> Alien sprites
سحبتها إلى المشروع في المجلد الجديد Player Sprites.
ثم عدت إلى مجلدات الحزمة، وسحبت الصور الآتية إلى المشروع:
ليصبح مجلد PlayerSprites كالآتي:
ولكن لدينا مشكلة صغيرة هنا وهي أن دقة الصور مختلفة ومتفاوتة، فحجم الشخصية عند استخدام صورة ما قد تختلف عن أخرى.
مع أن هذا لن يضر كثيرًا، لكن لكي أجعل الأرقام أسهل، سأقوم بتعديلها عن طريق تعديل خاصية Pixels Per Unit.
هكذا يصبح طول صورة الشخصية وهي واقفة يساوي 1 متر في اللعبة.
لنضع الصورة عند Character Sprite ونغير المواقع بما يَلزُم:
الآن وقد انتهينا من الترتيبات المهمة والمملة، لنبدأ العمل!
ما هو الأسلوب الذي سنتبعه للتحريك؟
الأسلوب التقليدي. التقليب بين صور مختلفة متقاربة بين بعضها، تسمى SpriteSheet، لإعطاء الوهم بأن هناك حركة سلسة طبيعية تحدث.
ولذلك حصلنا على عدد كبير من الصور، بخاصة للمشي.
لماذا لا تستخدم تقنيات أحدث؟
هذه التقنية، SpriteSheet Animation، مازالت منتشرة بكثرة، وهي ليست سيئة حيث تعطي نتائج ممتازة. فقط كونها قديمة لا يعني استبعادها. ولكن الحق يقال بأن التقنيات الأحدث مثل الـRigging تعتبر أفضل حيث تخفف عدد الصور المطلوبة. ولكنني أعتقد بأن استخدام التقنية التقليدية عند التعلم أمر مهم للتعرف على الموضوع بشكل أفضل.
سوف نقوم بتغيير صورة الشخصية بين حالات متعددة، في البداية تكون في الحالة الثابتة : الوقوف Idle.
وعندما يبدأ اللاعب في الضغط على زر المشي (يمينًا أو يسارًا)، نغير صورة الشخصية إلى أول صورة من صور المشي..
وبعد جزء من الثانية، نغير الصورة إلى الصورة الثانية، وبعد جزء آخر إلى الصورة الثالثة، وهكذا ما دام اللاعب مبقيًا على الزر.
عندما يفلت زر المشي، أي تتوقف الشخصية عن الحركة، نعيد صورة الشخصية إلى الصورة الأصلية Idle.
أيضًا، كلما زادت سرعة الشخصية – أي تغيرت من المشي إلى الركض – سوف نزيد من سرعة تغيير صور الشخصية.
وأخيرًا، عندما يقفز اللاعب، سنغير الصورة إلى صورة القفز، وعند الوقوع، نغيرها لصورة تمثل الهبوط، وعندما يلمس الأرض، نعيدها للصورة الأصلية.
هذه هي آلية تحريك الشخصية التي سنتبعها، وسنستخدم أدوات الـAnimation ومكونة الـAnimator في Unity لتطبيقها.
إضافة Animator و Animation Clip
سأشرح الـ Animator والـAnimator Controller لاحقًا، ولكن أولاً، لنذهب إلى واجهة الـAnimation بإحدى الطريقتين الآتيتين:
ثم نختار الشخصية من المشهد، نلاحظ ظهور زر Create في واجهة الـAnimation، نضغط عليه، تظهر لنا نافذة جديدة لكي نسمي فيها اسم الملف الجديد ونختار مجلد حفظ الملف.
سأحفظه في مجلد اسمه Animations داخل مجلد الـSources.
نسمي الملف Idle (أو Stand أو أي اسم آخر يعجبك) وهو الملف المسؤول عن وضع الشخصية الثابتة، دون أي حركة.
بعد الحفظ، بشكل تلقائي سيتكون لدينا ما يسمى بالـAnimator Controller في المجلد الذي اخترناه، وستُضاف مكونة الـAnimator إلى كائن الشخصية:
قبل أن نخوض في الـAnimator، لنتعرف على الـAnimation Window:
هي كأي واجهة للتحريك، لديك قائمة على اليسار بالخصائص، وعلى يمين كل خاصية (أي على خطها الأفقي) يمكنك أن تحدد الـKeyFrames على الخط الزمني.
كل Key Frame هو إطار، يحتوي على بيانات بقِيَم الخاصية عند هذا الإطار. هذه القيم نقوم نحن بتحديدها، وهذه الإطارات نضيفها أينما ومتى أردنا على الخط الزمني.
لتحديد (تسجيل) قيمة إطار معين، أولاً نضع المؤشر على الإطار المطلوب، ثم نضغط على الزر الأحمر Record، نقوم بتغيير خصائص الجسم (الشخصية في حالتنا). بعدها نضغط على الزر مرة أخرى.
ثم تُحفظ القيم في الملف Animation clip.
الفقرة الآتية مخصصة لمن لا يعرف مفهوم الـAnimation أو لم يتعامل معه سابقًا. يمكن الاختصار للقسم الذي يليها.
مثال لتوضيح آلية عمل الـKeyFrames:
إذا كانت لدينا على اليسار الخاصية Transform.position لجسم ما، فإن البيانات ستحتوي على ثلاثة متغيرات، x,y,z، وكل قيمة ستمثل الموقع على كل محور عند هذا الإطار.
يمكننا مثلا تحديد القيم عند الثانية 0 (عند أول نقطة على الإطار الزمني) بـ: (2,2,0) , وعند الثانية 1 نحددها بـ (5,4,3)
ثم، وهذه هي الفكرة من الـAnimation، عند تشغيل الملف، سيكون الموقع عند (2,2,0) وبعد مرور ربع ثانية، سيتم استقراء الموقع الحالي بناءً على الموقع النهائي.
أي أن الموقع على المحور x عند الثانية 0.25 سيكون:
Xi * (1 – t) + Xf * t = 2 * (1 – 0.25) + 5 * (0.25) = 2.75
إذًا، حصلنا على موقع للجسم عند هذه الثانية دون تحديده صراحة، وهكذا يتحرك الجسم من النقطة الأولى إلى الثانية بسلاسة، على فترات صغيرة، مما يعطي وهم الحركة.
تغيير الصورة للوضعية الثابتة في الملف Idle
واجهة الـAnimation تعمل أيضًا مع المتغيرات غير العددية، فمعظم الخواص لمعظم المكونات يمكن التحكم بها، مثل الألوان والصور.
في الملف Idle سوف نحدد صورة الشخصية بأنها الصورة Stand:
هكذا عندما يتم تشغيل الملف Idle، ستصبح صورة الشخصية Stand وتبقى كذلك حتى يتم تشغيل ملف آخر يغيرها.
إنشاء ملف Animation Clip للمشي Walk
كما ذكرت سابقًا، لمحاكاة المشي، سنتبع تسلسل الصور الذي حصلنا عليه سابقًا، عبر تغيير صورة الشخصية كل جزء صغير من الثانية، أو بمعنى آخر، كل إطار.
لانشاء ملف جديد، نضغط على اسم الملف الحالي في واجهة الـAnimation ثم يظهر لنا زر جديد اسمه Create New Clip
بعد ضغطه ستظهر لنا نافذة جديدة، سأحفظ الملف في المجلد Animations ذاته وأسميه Walk
لنقوم بإضافة صور المشي: نشغل التسجيل، نفتح المجلد الذي يحتوي على الصور، ثم نسحبها واحدة تلو الأخرى ونضعها محل الصورة في Character Sprite،
وبين كل صورة وأخرى، نحرك مؤشر الإطارات على الخط الزمني خطوة لليمين. كما هو موضح هنا:
والآن بعد أن أصبح لدينا ملفان – الوقوف والمشي – لنقم بربطهما مع بعضهما البعض عبر الـ Animator..
استخدام الـAnimator والـAnimator Controller
علينا أولاً فتح واجهة الـAnimator بنفس الطريقة التي فتحنا فيها واجهة الـAnimation. من:
Window –> Animation –> Animator
ثم لعرض ملف الشخصية، نذهب إليها ونفتح الـAnimator Controller عن طريق الضغط على خاصية Controller في الـAnimator:
وستظهر لنا هذه الواجهة (قد تحتاج لتغيير حجمها والتحرك داخلها حتى تظهر هذه العناصر) :
لاحظوا أن للملفان اللذان أنشأناهما، حصلنا على مستطلين كل واحد باسمه.
ماذا تمثل هذه المستطيلات؟ – شرح سريع عن نظام الـAnimator
يجب أن نعلم أولاً أن الـAnimator يعتمد على نظام تصميم يسمى بالـState Machine أو “آلة الحالات” إذا ما ترجمناه حرفيًا.
ما يقوم به هذا النظام هو تحديد حالات أو أوضاع يمكن أن يكون الكائن أو العنصر فيها، وتحديد الحالة الحالية، وعرض فرص للانتقال لحالات أخرى.
هذه الانتقالات قد تكون تلقائية أو قد تكون بأمر من المستخدم. وهي لا تحدث إلا عند توفر شروط معينة. وأشهر الشروط المستخدمة هو انتهاء الحالة الحالية.
فمثلاً إذا كنت تعرض حركتان متتابعتان لقفزة، تضع الحركة الأولى في حالة والثانية في حالة أخرى، وتضع شرطًا للانتقال من الأولى للثانية، وهو انتهاء الأولى.
كما يبدو، من الممكن برمجة كل هذا بأنفسنا. ما الفائدة من الـ Animator Controller إذًا؟
الفائدة تكون في أمور أكثر تعقيدًا. مثل الدمج بين الحالات عند الانتقال، ودمج أكثر من حالة في الوقت نفسه للحالة المحددة.
وهذه أمور مهمة جدًا عند تحريك الشخصيات عبر العظام، أي Rigging، وبرمجتها من الصفر يشكل تحديًا برمجيًا.
مع ذلك، نحن لا نحتاج لأساليب الدمج، لماذا سنستخدم الـ Animator Controller؟
هناك فوائد كثيرة:
- أولها أن النظام جاهز، فلا حاجة لنا أن نكتب أكوادًا وأصنافًا جديدة classes ونعيد محاكاة هذا النظام.
- سهولة إعداد الحالات والانتقالات عبر واجهة مرئية سلسة.
- سهولة إعداد انتقالات متعددة (أكثر من 2) مع شروط متعددة بين حالتان فقط، وهذا أمر سنحتاجه لاحقًا.
- سهولة كشف المشاكل وإصلاحها عند وجودها
هي السهولة لا أكثر.
أعتقد أنه من الواضح الآن أن المستطيلات أعلاه تمثل الحالات الممكنة: الوقوف، المشي، و البدء (عند التشغيل)
والحالة الأخيرة anyState تُستخدم لفرض الانتقال من أي حالة لحالة محددة عند تحقق شرط ما.
ما الفرق بين الـAnimator والـAnimator Controller؟
الـAnimator هو ملف برمجي أو مكوِّن جاهز يقوم بتشغيل ملفات الـAnimation وإيقافها على حسب الملف المتحكِّم Controller المعطى. فهو الذي يتحكم بشكل مباشر بالكائن أو الشخصية، إيقافه يوقف تشغيل الملفات، ويوفر خصائص تحدد كيفية تعامله مع الـController.
يمكن استخدام Controller واحد مع أكثر من Animator، ولكن ليس العكس.
باختصار، الأول هو المُشغل والثاني حافظ البيانات.
الربط بين الوقوف والمشي – إضافة معطيات Parameters
كما ذكرت في الأعلى، للانتقال من حالة لأخرى يجب تحقق شرط ما، وهنا للانتقال من الوقوف إلى المشي، سيكون الشرط كون السرعة أكبر من الصفر.
لكي نحصل على معلومات عن السرعة، سنستخدم الـParameter وهي معطيات نقوم بتعيينها من الملف البرمجي، يستطيع بعدها الـAnimator قراءة قيمها.
سنقوم بإضافة متغير xSpeed كالآتي: (السرعة على المحور الأفقي)
ثم سنقوم بالانتقال بين الوقوف والمشي بناء على هذا المتغير، فيما يلي توضيح ذلك:
ما قمت بفعله هو إضافة Transition أو انتقال بين الوقوف والمشي، بعدها ستظهر الخصائص في الـInspector
ألغيت تفعيل الـHas Exit Time لأننا لا نريد الانتقال عندما ينتهي كل ملف من دورته، (أي عندما ينتهي الـAnimation)
حددت مدة الانتقال Transition Duration بـ0 لأننا نريد ان يحدث الانتقال بكل مباشر دون تأخير.
وأخيرًا، عيّنت الشرط الذي تحدثت عنه، وهو كون السرعة أكبر من الصفر.
وفي المقابل، الانتقال من المشي إلى الركض، قمت بهذه الأمور ذاتها ولكن عكست الخطوة الأخيرة.
لماذا يوجد خيار “أكبر من” و “أصغر من” ولكن لا يوجد خيار “يساوي”؟
لأننا نتعامل مع Floats، أعداد كسرية. هذه الأعداد تخضع لحد من الدقة، يسمى Floating Point Precision. دون الخوض في التفصيل، المشكلة أنه من الممكن أن تكون قيمة المتغير xSpeed قريبة جدًا من الصفر ولكن لا تساويها، مثلا 0.00001، وللحفاظ على الدقة، يبقيها البرنامج بهذه القيمة. وعندما نقارنها بالصفر، نجد أنها لا تساوي الصفر. مع أنه من المفترض أن نقف لأن الشخصية تعتبر واقفة بهذه السرعة.
كل ما بقي لدينا الآن هو تعيين المعطى Parameter من الملف البرمجي.
إنشاء ملف برمجي خاص بالحركيات للشخصية
سأنشئ ملفًا برمجيًا أسميه PlayerAnimation وأضيفه إلى الشخصية.
في البداية، سأعرّف الأمور الضرورية، وهي مرجع للـ Animator، ومرجع للـ PlayerManager.
وكما اعتدنا في الملفات البرمجية السابقة، سنكوّن دالة Initialize تستلم مرجع الملف المدير:
using UnityEngine; public class PlayerAnimation : MonoBehaviour { PlayerManager playerManager; Animator animator; void Start() { animator = GetComponent<Animator>(); } public void Initialize(PlayerManager _playerManager) { playerManager = _playerManager; } }
تذكروا أننا يجب أن نمرر قيمة تمثل السرعة الحالية على المحور الأفقي إلى الـAnimator.
سنفعل ذلك باستخدام الدالة SetFloat الموجودة في الصنف Animator، وهي تأخذ معطيين: اسم المتغير والقيمة.
ولكن هل علينا تمرير قيمة السرعة ذاتها؟ السرعة قد تزيد إلى قيم كبيرة وهذا سيسبب مشاكل لاحقًا، لأننا وكما ذكرت سنغير سرعة الأنيمشين بناءً على سرعة الشخصية.
للك سنمرر مبدئيًا مطلق قيمة الإدخال horizontalMovingInput التي تترواح بين -1 و 1 دائمًا. نمررها مطلقة لأننا نريد “السرعة” وليس “السرعة المتجهة”.
سنفعل ذلك في دالة جديدة نسميها Update Animation وبعدها سنستدعيها من الملف PlayerManager في المكان المناسب.
في الملف PlayerAnimation:
... [SerializeField] string speedParameter = "xSpeed"; ... public void UpdateAnimation() { animator.SetFloat(speedParameter, Mathf.Abs(playerManager.inputHandler.horizontalMovingInputValue)); }
ثم في الملف PlayerManager سنأخذ مرجعًا للملف PlayerAnimation ونستدعي الدالة UpdateAnimation كل إطار من الدالة Update.
... public PlayerAnimation playerAnimation { get; private set; } void Awake() { ... playerAnimation = GetComponent<PlayerAnimation>(); playerAnimation.Initialize(this); } void Update() { inputHandler.CheckClickInput(); playerAnimation.UpdateAnimation(); }
لماذا Update وليس Fixed Update؟
لأن إطارات اللعبة Update دائما متزامنة مع العرض على الشاشة، والحركيات هي مجرد تأثير، ناتج عن أوامر وفيزياء اللعبة، والتأثيرات دائما يجب أن تُحَدّث في إطارات اللعبة لكي تبقى متزامنة مع العرض، هذه قاعدة عامة.
في الحقيقة، يمكن استدعاء الدالة في إطارات الفيزياء، ولكن قد تسبب ضغطًا على الأداء، لأن الفيزياء قد تُحَدّث أكثر من مرة في إطار لعبة واحد، ولهذا قد تُحَدّث معلومات الحركيات أكثر من مرة بلا أي فائدة.
تذكروا إضافة الملف PlayerAnimation إلى الشخصية!
رائع، الآن ستتغير صورة الشخصية عند المشي، ولكن هناك مشكلة، ستلاحظوا بأن تغير الصور يحدث بمعدل بطيء. مما يعطي إيحائًا بأن الشخصية بطيئة.
لحل المشكلة، قمت بتغيير قيمة الـSamples إلى 30:
موازنة سرعة تقليب الصور بسرعة الشخصية
لاحظوا بأنه عند الضغط على زر المشي ولو للحظة واحدة، فإن الصور ستتقلب ويبدو أن الشخصية تمشي بسرعة.
ما نريده هو تغيير سرعة تقليب الصور بناءً على سرعة الشخصية، ولحسن الحظ، يمكننا فعل ذلك بسهولة بأحد مزايا الـAnimator
نذهب إلى واجهة الـAnimator ونختار الحالة Walk، وعند الخصائص على الـInspector، نفعل خيار Parameter الأول عند المتغير xSpeed.
سيكون المتغير مختارًا بالأصل، لأنه ليس لدينا سوى متغير واحد، لذلك كل ما علينا فعله هو الضغط على الـCheckbox:
ماذا تفعل هذه الخاصية؟ تضرب سرعة الملف الأصلي بالقيمة xSpeed.
لذلك عندما تكون الشخصية بطيئة، ستتقلب الصور بمعدل بطيء، وعند زيادة سرعتها، يزيد معدل تغيير أو قلب الصور.
زيادة سرعة التقليب أكثر عند الركض
مع أن السرعة متوازنة الآن، هذا فقط عند المشي. أما عند الركض، وزيادة سرعة الشخصية، فإن القيمة xSpeed ستبقى 1 ولن تزيد عنها.
لحل هذه المشكلة، سنقوم بضرب قيمة xSpeed برقم ثابت أثناء الركض.
ما هو هذا الثابت؟ يمكننا تعريفه بشكل مستقل، ولكن لتخفيف الثوابت و”الأرقام العجيبة”، سأستخدم النسبة بين سرعة الركض وسرعة المشي.
تذكروا انه في الملف PlayerMovement لدينا متغير لسرعة المشي وآخر لسرعة الركض.
سأنشئ دالتيّ Get لهذان المتغيران لأستطيع أن أصل إليهما من الملف PlayerAnimation.
في الملف PlayerMovement نضيف الآتي ( تأكدوا من استخدام الأسماء الصحيحة للمتغيرات! ) :
... public float GetWalkSpeed(){ return walkSpeed; } public float GetRunSpeed(){ return runSpeed; } ...
ثم في الملف PlayerAnimation داخل الدالة UpdateAnimation:
void UpdateAnimation(){ //متغير يحدد معامل زيادة السرعة float speedModifier = 1; //إذا كانت الشخصية تركض if(playerManager.inputHandler.isHoldingRunInput){ //النسبة بين سرعة الركض والمشي speedModifier = playerManager.playerMover.GetRunSpeed() / playerManager.playerMover.GetWalkSpeed(); } //نضرب قيمة هذا المتغير بقيمة الإدخال الأصلية animator.SetFloat(speedParameter, Mathf.Abs(speedModifier * playerManager.inputHandler.horizontalMovingInputValue)); }
ملاحظة: يفضل القيام بالعملية الحسابية في السطر 7 أعلاه مرة واحدة فقط، مثلا في الدالة Start، وحفظ القيمة بمتغير.
إذا لم تعجبكم هذه السرعة الآن، بالإمكان إضافة متغير جديد بدلاً من حساب النسبة، أو يمكن تغيير سرعة الملف Walk من داخل الـAnimator، هكذا:
إنشاء ملف Animation للقفز
بنفس الطريقة السابقة، سأنشئ ملف Animation جديد للقفز وأسميه Jump. وسأحفظه في المجلد Animations ذاته.
بعد إنشاء الملف، ستتم إضافة حالة باسم Jump إلى واجهة الـAnimator تلقائيًا.
والآن، سنقوم بالتسجيل للملف وتغيير صورة الشخصية إلى صورة القفز عند بداية هذا الملف.
استخدمت الصورة alienGreen_jump التي حصلنا عليها سابقًا:
إضافة انتقال إلى ملف القفز في الـAnimator
نريد إضافة انتقال من الحالات الأخرى – المشي والوقوف – إلى حالة القفز، ولكن متى؟ بأي شرط سننتقل؟
يجب أن نضيف مُعطى يخبرنا ما إذا كانت الشخصية تقفز، لذلك سنضيف Parameter نوعه Trigger نسميه Jumped أو ما شابه.
الـTrigger يشبه المنبه، تعمل إشارته لمرة واحدة فقط، وعند الانتقال من حالة لأخرى بناءً على هذا المنبه، تتوقف الإشارة.
عند تفعيل إشارة Jumped، سننتقل من حالة الوقوف أوحالة المشي إلى حالة القفز.
ولكن، كيف سنعود من حالة القفز إلى حالة الوقوف (أو المشي) ؟ يجب أيضًا أن نضيف مُعطى آخر يخبرنا ما إذا وصلت الشخصية إلى الأرض.
لذلك سنضيف Parameter جديد نوعه Bool أسميه Grounded. سيكون بقيمة صحيحة true إذا كانت الشخصية ملامسة للأرضية.
إذا كنا في حالة القفز، سنعود إلى حالة الوقوف أو المشي إذا كان المتغير Grounded صحيح.
لنضيف الآن الانتقالات الآتية بين الحالات بناءً على هذا المتغير:
- من الحالة Idle إلى الحالة Jump عندما تفعل إشارة Jumped
- من الحالة Walk إلى الحالة Jump عندما تفعل إشارة Jumped
- من الحالة Jump إلى الحالة Idle عندما يكون Grounded = true
- من الحالة Jump إلى الحالة Walk عندما تفعل إشارة Grounded = true و تكون xSpeed > 0
سنواجه مشكلة صغيرة هنا وهي أن الانتقال من الحالة Jump إلى Walk لن يحدث مباشرة أبدًا، لأن الانتقال من Jump إلى Idle سيكون هو المُهَيمن.
مع أنها لن تؤثر حقًا، من الأفضل أن نحلها هكذا:
التحكم برمجيًا بانتقالات حالة القفز
في الملف البرمجي PlayerAnimation، سنضيف متغيرين نصيّين، لمُعطى منبه الانتقال لحالة القفز، ولمُعطى ملامسة الأرضية.
وسنضيف دالة في PlayerManager نقوم باستدعائها عند القفز من الملف PlayerMovement ثم هي بدورها تستدعي دالة في الملف PlayerAnimation.
PlayerAnimation:
... [SerializeField] string jumpTrigger = "Jumped"; [SerializeField] string groundedStatusParameter = "Grounded"; public void UpdateAnimation() { ... //نحدد قيمة معطى ملامسة الأرض بنفس قيمة متغير ملامسة الأرض الموجود في الملف المدير animator.SetBool(groundedStatusParameter, playerManager.grounded); } // public void Jumped() { animator.SetTrigger(jumpTrigger); }
في الملف PlayerManager نضيف دالة جديدة
... //يجب أن تستدعى هذه الدالة عندما تقفز الشخصية public void Jumped(){ playerAnimation.Jumped(); }
ثم في الملف PlayerMovement في الدالة DoJump:
public void DoJump() { ... if (inputHandler.pressedJump && inputHandler.pressedHorizontalInput && playerManager.foundWall) { ... if (x_axis < 0 == normalRightDot < 0){ ... playerManager.Jumped(); } } if (inputHandler.pressedJump && playerManager.grounded && !hasJumped && !jumpedOffWall){ ... playerManager.Jumped(); } }
إنشاء ملف Animation للهبوط
عندما تعود الشخصية إلى الأرضية، نريد أن نعرض صورة مختلفة تعبر عن الوقوع أو الهبوط.
لا يوجد صورة مخصصة لهذه الوضعية ضمن الصور التي نزلناها سابقًا، ولكن يمكن استخدام أي صورة مناسبة، وبالنسبة لي، سأستخدم الصورة الآتية:
وبالطريقة ذاتها، نضيف ملف Animation سأسميه Fall، ونغير فيه صورة الشخصية إلى هذه الصورة:
مرة أخرى، ستُضاف حالة جديد بنفس الاسم Fall إلى الـAnimator تلقائيًا.
ثم في الـAnimator، سنضيف انتقالاً من حالة القفز إلى هذه الحالة، عندما تبدأ الشخصية في الهبوط.
كيف؟ نريد أن نعرف إذا كانت السرعة الرأسية سالبة، أو أقل من -1، عندها نقوم بالانتقال. لذلك سنضيف مُعطى جديد ySpeed:
وأيضًا، في حال كانت الشخصية غير ملامسة للأرضية، ولم تقفز، فليس لدينا خيار سوى الانتقال لهذه الحالة.
ستصبح لدينا الانتقالات الآتية:
- من Jump إلى Fall عند ySpeed < -1
- من Fall إلى Jump عندما تفعل الإشارة Jumped (تذكروا الارتداد عن الجدران)
- من Fall إلى Idle عندما Grounded = true و xSpeed < 0.01
- من Fall إلى Walk عندما Grounded = true و xSpeed > 0
- من Idle إلى Fall عندما Grounded = false
- من Walk إلى Fall عندما Grounded = false
لن أقوم بعرض تطبيق جميع هذه الانتقالات، اعتبروها تدريبًا!. واتبعوا نفس أسلوب المرة السابقة، تذكروا إلغاء خيار Has Exit Time وتعيين القيمة 0 لـTransition Duration في جميع الانتقلات.
يجب أن تحصلوا على شيء مشابه لهذا (بغض النظر عن الترتيب):
التحكم برمجيًا:
في الملف البرمجي PlayerAnimation سضيف متغيرًا لاسم مُعطى السرعة الرأسية، ونعطيه قيمة السرعة من الـRigidbody2d:
[SerializeField] string ySpeedParameter = "ySpeed"; public void UpdateAnimation() { ... animator.SetFloat(ySpeedParameter, playerManager.rig2D.velocity.y); }
الآن سيصبح اللعب ممتعًا.
مشكلة تكرار إشارة Jumped
أحد المشاكل التي لا تظهر بشكل مباشر هي تكرار تشغيل المنبه Trigger الخاص بالقفز.
يحدث هذا عند طلب القفز أثناء عمل حالة القفز، أي أثناء القفز نفسه، وفي لعبتنا، يحدث عند الارتداد عن الجدران مرات متتالية.
المشكلة هنا أن الإشارة ستبقى مفعلة، ثم بعد الانتقال إلى حالة أخرى (من القفز) إلى المشي مثلاً، ستعود الشخصية وتنتقل إلى حالة القفز لإطار.
حاولت التقاط صورة للمشكلة ولم أستطع، فهي لا تظهر حقًا، ولكن من الضروري حل المشكلة لأنها قد تسبب تبعات لاحقة.
لحلها ببساطة، قبل تفعيل مؤشر القفز، نتفحص الحالة الحالية، فإذا كانت الحالة هي حالة القفز، لا نفعل المؤشر ثانية:
في الملف PlayerAnimation بداخل الدالة Jumped:
[SerializeField] //قم بوضع اسم حالة القفز هنا string jumpStateName = "Jump"; public void Jumped() { //نستخدم هذه الدالة للحصول على معلومات عن الدالة الحالية if(animator.GetCurrentAnimatorStateInfo(0).IsName(jumpStateName)){ return; } ... }
شرح الدالة GetCurrentAnimatorStateInfo
تأخذ هذه الدالة مُعطى واحد وهو مؤشر الـLayer أو الطبقة في الـAnimator.
بالنسبة لدينا نحن نستخدم طبقة واحدة فقط، لذا المؤشر يكون 0 (العد في البرمجة يبدأ من الصفر)
ثم تعطينا هذه الدالة معلومات عن الحالة الحالية، عن طريق إرجاع كائن من النوع AnimatorStateInfo
يحتوي هذا النوع على دالة اسمها IsName، والتي تأخذ اسم الحالة التي يراد التحقق منها، وتخبرنا ما إذا كانت هذه هي الحالة المشغلة الآن في الـAnimator.
هكذا نكون قد انتهينا من الحركيات الأساسية للشخصية، في المقال القادم إن شاءالله سنضيف حركات و”ميكانيكيات” متقدمة.
كيف تغيرت نظرتكم نحو هذه اللعبة المصغرة بعد إضافة الحركيات؟ ترقبوا إضافة بيئة للعبة، ستزيد الحيوية أضعافًا.
أعطوني رأيكم بالمقال في التعليقات، وإذا واجهتم أي مشكلة في تطبيق محتوى المقال، اطلبوا المساعدة في التعليقات أيضًا.