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

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

ترتيب معالجة العقد في شجرة المشاهد

يتضمن محرك ألعاب جودو مفهوم يسمى شجرة المشاهد Scene Tree تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي:

01 tree order 01

سنرفق هذا الكود بكل عقدة:

extends Node

func _init():
    # ملاحظة: العقدة ليس لها اسم بعد هنا
    print("TestRoot init")

func _enter_tree():
    print(name + " enter tree")

func _ready():
    print(name + " ready")

# ‫يضمن ما يلي أننا نطبع مرة واحدة فقط في process()‎
var test = true
func _process(delta):
    if test:
        print(name + " process")
    test = false

يوضح الكود أعلاه كيفية تفاعل العقدة مع الأحداث المختلفة في شجرة المشاهد، ولنوضح ما يمثله استدعاء كل دالة من الدوال الواردة فيه قبل أن نتحدث عن النتائج:

  • تستدعى الدالة ‎_init()‎ عند إنشاء العقدة أو الكائن لأول مرة، وتكون العقدة حينها موجودة في ذاكرة الحاسوب ولم تُضف لشجرة المشاهد
  • تستدعى الدالة ‎_enter_tree()‎ عند إضافة العقدة للشجرة لأول مرة، ويمكن أن يحدث ذلك عند إنشاء نسخة من العقدة أو عند إنشاء عقدة ابن من عقدة ما باستخدام التابع add_child()‎ مثلًا
  • تستدعى الدالة ‎_ready()‎ عند اكتمال إضافة العقدة وأبنائها بنجاح لشجرة المشاهد وجاهزيتها للعمل
  • تستدعى الدالة ‎_process()‎ بشكل دوري في كل إطار -60 مرة في الثانية عادةً- وذلك لكل عقدة في الشجرة ويمكن استخدامها للتعامل مع التحديثات المتكررة

إذا شغّلنا الكود على عقدة واحدة بمفردها، فسيكون ترتيب استدعاء الدوال كالمتوقع وفق ما يلي:

TestRoot init
TestRoot enter tree
TestRoot ready
TestRoot process

لكن إذا أضفنا عقد أبناء، فسيصبح الأمر أكثر تعقيدًا وقد يحتاج إلى بعض التوضيح:

TestRoot init
TestChild1 init
TestChild3 init
TestChild2 init

TestRoot enter tree
TestChild1 enter tree
TestChild3 enter tree
TestChild2 enter tree

TestChild3 ready
TestChild1 ready
TestChild2 ready
TestRoot ready

TestRoot process
TestChild1 process
TestChild3 process
TestChild2 process

طبعت جميع هذه العقد رسائلها بترتيب الشجرة من الأعلى إلى الأسفل باستثناء الشيفرة البرمجية الخاصة بالتابع ‎_ready()‎ والذي يُستدعَى عندما تكون العقدة جاهزة، أي عندما تدخل العقدة وأبناؤها شجرة المشاهد، فإذا كان للعقدة أبناء، فستُشغَّل استدعاءات التابع ‎_ready الخاصة بالأبناء أولًا، وستتلقى العقدة الأب إشعار الجاهزية بعد ذلك كما يوضح توثيق جودو الرسمي.

يقودنا ذلك إلى قاعدة أساسية مهمة يجب تذكرها عند إعداد بنية العقد وهي: يجب أن تدير العقد الأب أبناءها وليس العكس، ويجب أن تكون أيّ شيفرة برمجية للعقدة الأب قادرةً على الوصول الكامل إلى بيانات أبنائها، لذا يجب معالجة استدعاءات التابع ‎_ready()‎ بترتيب الشجرة العكسي.

علينا تذكّر ذلك عند محاولة الوصول لعقد أخرى في التابع ‎_ready()‎، فإذا كنا بحاجة للانتقال لأعلى الشجرة إلى عقدة أب أو عقدة جَد، فيجب تشغيل هذه الشيفرة البرمجية في العقدة الأب وليس في العقدة الابن.

فهم مسارات العقد والتنقل في شجرة المشاهد

تُستخدم مسارات العقد Node Paths في جودو لفهم كيفية التنقل بين العقد في شجرة المشاهد Scene Tree. وهذه المسارات أساسية لفهم كيفية الوصول للعقد المختلفة داخل الشجرة وتجنب مشكلة وجود مرجع عقدة غير صالح، والتي تظهر على هيئة رسالة خطأ كالتالي:

Invalid get index ‘position’ (on base: ’null instance’).

يُعَد الجزء الأخير من رسالة الخطأ null instance مصدر هذه المشكلة، وهو يسبب إرباكًا للمبتدئين في جودو، ويمكن تجنّب هذه المشكلة من خلال فهم مفهوم مسارات العقد.

مسارات العقد

تتكون شجرة المشهد من عقد ترتبط ببعضها البعض بعلاقات أب-ابن، ومسار العقد هو المسار المُتّخَذ للانتقال من عقدة إلى أخرى من خلال التحرّك عبر هذه الشجرة. لنأخذ مثلًا مشهد لاعب بسيط كما يلي:

02 node paths 01

يوجد كود هذا المشهد في العقدة Player. إذا كان السكربت بحاجة إلى استدعاء الدالة play()‎ مع العقدة AnimatedSprite، فسيحتاج إلى مرجع إلى تلك العقدة:

get_node("AnimatedSprite").play()

إن وسيط الدالة get_node()‎ هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ null instance وخطأ عدم العثور على العقدة Node not found أيضًا.

يُعَد الحصول على مرجع عقدة باستخدام الدالة get_node()‎ حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة $ للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة AnimatedSprite‎ وتشغيل الدالة ()play عليها مباشرة نكتب:

$AnimatedSprite.play()

ملاحظة: تعيد الدالة get_node()‎ مرجعًا Reference إلى العقدة المطلوبة.

لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي:

03 node paths 02

إذا احتاج الكود المرفق بالعقدة Main إلى الوصول إلى العقدة ScoreLabel، فيمكنه ذلك باستخدام هذا المسار:

get_node("HUD/ScoreLabel").text = "0"
# ‫أو باستخدام الاختصار:
$HUD/ScoreLabel.text = "0"

ملاحظة: سيكمل محرر جودو المسارات تلقائيًا نيابةً عنا عند استخدام صيغة $، ويمكننا أيضًا النقر بزر الفأرة الأيمن على عقدة ما في تبويب المشهد Scene واختيار نسخ مسار العقدة Copy Node Path.

إذا كانت العقدة التي نريد الوصول إليها موجودة في مكان أعلى من الشجرة، فيمكننا استخدام الدالة get_parent()‎ أو ".." للإشارة إلى العقدة الأب، حيث يمكننا الحصول على العقدة Player من العقدة ScoreLabel في شجرة المثال السابق كما يلي:

get_node("../../Player")

يمثّل المسار ‎"../../Player"‎ الحصول على العقدة التي تقع في مستوى واحد أعلى HUD ثم العقدة التي تقع في مستوى أعلى وهي Main ثم الوصول إلى العقدة الابن لها وهي Player.

ملاحظة: تعمل مسارات العقد مثل مسارات المجلدات في نظام التشغيل، حيث تشير الشرطة المائلة / إلى علاقة أب-ابن، وتعني .. مستوى واحد أعلى.

المسارات النسبية Relative والمسارات المطلقة Absolute

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

get_node("/root/Main/Player")

لا يعيد المسار ‎/root والذي يمكن الوصول إليها أيضًا باستخدام get_tree().root العقدة الجذر لمشهدنا الحالي، ولكنه يعيد العقدة الجذر لنافذة العرض Viewport التي توجد دائمًا في شجرة المشهد SceneTree افتراضيًا.

مشكلة في التعامل مع مسارات العقد في جودو

تعمل الأمثلة السابقة بنجاح، ولكن توجد بعض الأشياء التي يجب أن نكون على دراية بها والتي قد تسبب مشكلات لاحقًا. لنفترض أن لدينا الحالة التالية: تحتوي العقدة Player على الخاصية health التي نريد عرضها في العقدة HealthBar في مكان ما في واجهة المستخدم الخاصة بنا، لذا يمكن كتابة شيء يشبه ما يلي في كود اللاعب:

func take_damage(amount):
    health -= amount
    get_node("../Main/UI/HealthBar").text = str(health)

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

  1. لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة get_node()‎ في حدوث عطل
  2. لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره

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

الخاتمة

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

ترجمة -وبتصرّف- للقسمين Understanding tree order و Understanding node paths من توثيقات Kidscancode.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...