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

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

الحركات داخل جدول الشخصيات SpriteSheet

تُعد جداول الشخصيات Spritesheets من الطرق الشائعة لتوزيع الحركات للرسوم المتحركة ثنائية البعد، إذ توضع جميع إطارات الشخصية ضمن صورة واحدة. سنستخدم في مقالنا شخصية المغامر، ويمكن الحصول عليها وعلى غيرها من الشخصيات من متجر Elthen's Pixel Art Shop الذي يوفر ملحقات مفيدة يمكن استخدامها في تطوير الألعاب.

01_adventurer_sprite_sheet_v1.1.png

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

إعداد عقد التحريك

تستخدم تقنية التحريك العقدة Sprite2D لعرض الخامة، بعدها نحرّك الإطارات المتغيرة باستخدام العقدة AnimationPlayer. يمكن لهذا الترتيب أن يعمل على أي عقدة ثنائية البعد، لكننا سنستخدم في مثالنا العقدة CharacterBody2D.

لنضف العقد التالية إلى المشهد:

CharacterBody2D: Player
  Sprite2D
  CollisionShape2D
  AnimationPlayer

نسند جدول الشخصيات إلى الخاصية Texture للعقدة Sprite2D وسنلاحظ أن الجدول بكامله قد ظهر في نافذة العرض. ولكي نقطعه إلى إطارات فردية، نوسّع قسم التحريك Animation في نافذة الفاحص وناضبط قيم الخاصيتين Hframes و Vframes على 13 و 8 على الترتيب، وهما يمثلان عدد الإطارات الأفقية والعمودية في جدول الشخصيات:

02 sprite animation frames

لنجرّب تغيير الخاصية Frame لمراقبة تغيّر الصورة، فهي الخاصية التي سنعمل على تحريكها.

تحريك الشخصية

سنختار العقدة AnimationPlayer ثم ننقر الزر تحريك Animation يتبعه الزر جديد New ونسمي الحركة الجديدة idle. نضبط بعد ذلك مدة الحركة على 2 ثانية وننقر الزر Loop كي تتكرر الحركة باستمرار.

نجعل شريط التقدم Scrubber عند الزمن 0 ثم نختار العقدة Sprite2D. نضبط الخاصية Animation>Frame على 0 ثم ننقر على أيقونة المفتاح إلى جوار القيمة.

03 sprite animation frames

إن حاولنا الآن تشغيل الحركة فلن نرى شيئًا، لأن الإطار الأخير رقم 12 يبدو مشابهًا للإطار الأول رقم 0. مع ذلك لم نتمكن من رؤية الإطارات بينهما. لإصلاح الأمر نغير الخاصية Update Mode للمسار من القيمة الافتراضية Discrete إلى Continuous وسنجد هذا الزر في نهاية المسار من الجانب الأيمن.

04 sprite animation track

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

11_animation_tree_control.gif

يمكن تجربة وضع حركات أخرى مثل حركة القفز التي نجد صورها في الإطارات من 65 إلى 70.

استخدام المتحكم AnimationTreeStateMachine

لنتخيل أن لدينا كم كبير من الحركات، وأصبح من الصعب علينا التحكم عملية التنقل فيما بينها، وامتلأ السكريبت بعبارات if وكلما أردنا تصحيح شيء أخفق ما تبقى. لحل الأمر نستخدم العقدة AnimationTree لإنشاء مُتحكّم يسمح لنا بترتيب الحركات المختلفة للشخصية وإدارة عملية التنقل فيما بينها.

سنستخدم في مثالنا نفس شخصية المغامر التي استخدمناها في المثال السابق، ونفترض أننا هيأنا مسبقًا حركات الشخصية باستخدام العقدة AnimationPlayer. وعندما نستخدم جدول الشخصيات السابق سنجد صورًا توافق الحركات التالية:

  • سكون idle 
  • ركض run 
  • هجوم attack1 
  • هجوم attack2 
  • إصابة hurt 
  • موت die

استخدام شجرة الرسوميات AnimationTree

نضيف العقدة AnimationTree إلى المشهد ثم نختار New AnimationNodeStateMachine من الخاصية TreeRoot.

06 animation tree

تتحكم العقدة AnimationTree بالرسوميات التي تنشأ ضمن العقدة AnimationPlayer، ولكي نسمح لها بالوصول إلى الرسوميات الموجودة، ننقر على الخاصية Assign ضمن الخاصية Anim Player ثم نختار عقدة الحركة.

يمكننا الآن إعداد متحكم التنقل ضمن نافذة AnimationTree:

07 anim tree panel

ننتبه إلى التحذير الظاهر، ونضبط الخاصية Active في نافذة الفاحص على القيمة On ثم ننقر بعد ذلك بالزر اليميني للفأرة ونختار Add Animation. نختار بعد ذلك الحركة idle وسنرى صندوقًا صغيرًا يمثل هذه الحركة. نكرر نفس العملية لإضافة مثل هذه الصناديق إلى بقية الحركات.

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

08 animation tree connection

عندما تختار حركة، ستتبع الشجرة المسار الذي يصل العقدة الحالية إلى الوجهة. لكن في طريقة إعداد المثال السابق، لن نرى الهجوم الأول attack1 إن شغلنا الهجوم الثاني attack2. يعود السبب في ذلك إلى أن نمط التبديل switch mode للاتصال نوعه مباشر immediate. لهذا، ننقر على زر Move/select ثم ننقر على الاتصال بين attack1 و attack2 ثم نغير من نافذة الفاحص الخاصية Switch Mode إلى At End ونكرر ذلك على الاتصال بين attack2 و idle.

ما يحدث الآن أنه عند تشغيل في AnimationTree، أنه عند الانتقال من idle إلى attack2، يجري تشغيل الحركتين attack1 و attack2 على التتابع، ولكن بعد ذلك تتوقف الرسوم المتحركة عند attack2 بدلاً من العودة تلقائيًا إلى idle. لحل هذه المشكلة، نضبط الخاصية Advance>Mode  على Auto مما يسمح للشجرة بالعودة إلى الحركة idle بشكل تلقائي بعد تنفيذ حركتي الهجوم attack، ونلاحظ أن أيقونة الاتصال تتحول إلى اللون الأخضر لإظهار ذلك.

09_animation_tree_loop.gif

وهكذا ستُتنفذ الحركات على التتابع بمجرد تفعيلها.

استدعاء الحالات في الشيفرة

فيما يلي شجرة الحركات بأكملها:

10 animation tree full

لنهيئ الآن الشخصية كي تستخدم هذه الحركات:

extends CharacterBody2D

var state_machine
var run_speed = 80.0
var attacks = ["attack1", "attack2"]

@onready var state_machine = $AnimationTree["parameters/playback"]

تضم الخاصية state_machine مرجعًا إلى المتحكم بالحالة وهو AnimationNodeStateMachinePlayback، ولاستدعاء حركة محددة، نستخدم التابع travel الذي سيتّبع الاتصالات إلى الرسم المتحرك المحدد:

func hurt():
    state_machine.travel("hurt")

func die():
    state_machine.travel("die")
    set_physics_process(false)

لدينا هنا مثال عن الدوال التي قد نستدعيها، إن أصيب اللاعب أو قتل. وبالنسبة إلى بقية الحالات كالركض والهجوم وغيرها فلا بد من جمعها مع شيفرة الحركة وشيفرة معالجة المدخلات. وستحدد الخاصية velocity إن كنا سنرى حالة حركة الركض run أو حركة السكون idle:

func get_input():
    var current = state_machine.get_current_node()
    velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * run_speed
    if Input.is_action_just_pressed("attack"):
      state_machine.travel(attacks.pick_random())
      return
    # اقلب الشخصية من اليمين إلى اليسار
    if velocity.x != 0:
      $Sprite2D.scale.x = sign(velocity.x)
    # اختر رسمًا متحركًا
    if velocity.length() > 0:
      state_machine.travel("run")
    else:
      state_machine.travel("idle")
    move_and_slide()

نلاحظ استخدام return بعد الانتقال إلى حركة الهجوم كي نتمكن من الانتقال إلى حالات الحركة أو السكون لاحقًا في الدالة.

الخاتمة

تعرفنا في هذا المقال على طريقة استخدام SpriteSheet في جودو لتوليد حركات مختلفة للشخصية، كما تعرفنا على استخدام AnimationTree Animation Tree State Machine في إدارة التنقل بين الرسوميات المختلفة للشخصية. وبإمكانك من الاطلاع على المشروع بصيغته المكتملة لتنفيذه وفهمه بصورة أفضل.

ترجمة -وبتصرف- للمقالين: SpriteSheet ANimation و Using AnimationTreeStateMachine

اقرأ أيضًا


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...