سنشرح في هذا المقال آلية معالجة العقد ضمن شجرة المشاهد والترتيب الذي يتبعه محرك الألعاب جودو Godot للتعامل معها، كما سنوضح ما هي مسارات العقد وكيفية التنقل بينها، سيساعدنا ذلك على فهم طريقة تنظيم لعبتنا، والتحكم بها بفعالية أكبر.
ترتيب معالجة العقد في شجرة المشاهد
يتضمن محرك ألعاب جودو مفهوم يسمى شجرة المشاهد Scene Tree تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي:
سنرفق هذا الكود بكل عقدة:
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 مصدر هذه المشكلة، وهو يسبب إرباكًا للمبتدئين في جودو، ويمكن تجنّب هذه المشكلة من خلال فهم مفهوم مسارات العقد.
مسارات العقد
تتكون شجرة المشهد من عقد ترتبط ببعضها البعض بعلاقات أب-ابن، ومسار العقد هو المسار المُتّخَذ للانتقال من عقدة إلى أخرى من خلال التحرّك عبر هذه الشجرة. لنأخذ مثلًا مشهد لاعب بسيط كما يلي:
يوجد كود هذا المشهد في العقدة Player
. إذا كان السكربت بحاجة إلى استدعاء الدالة play()
مع العقدة AnimatedSprite
، فسيحتاج إلى مرجع إلى تلك العقدة:
get_node("AnimatedSprite").play()
إن وسيط الدالة get_node()
هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ null instance
وخطأ عدم العثور على العقدة Node not found
أيضًا.
يُعَد الحصول على مرجع عقدة باستخدام الدالة get_node()
حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة $
للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة AnimatedSprite
وتشغيل الدالة ()play
عليها مباشرة نكتب:
$AnimatedSprite.play()
ملاحظة: تعيد الدالة get_node()
مرجعًا Reference إلى العقدة المطلوبة.
لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي:
إذا احتاج الكود المرفق بالعقدة 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)
قد يكون هذا السكربت جيدًا في البداية، ولكن يمكن أن يواجه خطأ بسهولة، إذ توجد مشكلتان رئيسيتان في هذا النوع من الترتيب وهما:
-
لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة
get_node()
في حدوث عطل - لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره
لذا علينا تجنب استخدام مسارات العقد التي تنتقل إلى الأعلى في شجرة المشهد. إذا أصدر اللاعب في المثال السابق إشارة عند تغير مستوى صحته health، فيمكن لواجهة المستخدم الاستماع إلى هذه الإشارة لتحديث نفسها، ثم يمكننا إعادة ترتيب العقد والفصل بينها دون الخوف من توقف اللعبة.
الخاتمة
نأمل أن يكون هذا المقال قد ساعدكم على تكوين فكرة واضحة حول استخدام مسارات العقد في جودو، والطريقة الصحيحة للتنقل بين العقد والاتصال بالعناصر التي تحتاجوها في شجرة المشاهد. ففهم مسارات العقد هو الأساس الذي يمكننا البناء عليه لتفادي العديد من الأخطاء الشائعة ورسائل الخطأ مثل null instance
والإشارة لأي عقدة نحتاجها بالطريقة الصحيحة.
ترجمة -وبتصرّف- للقسمين Understanding tree order و Understanding node paths من توثيقات Kidscancode.
اقرأ أيضًا
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.