بعد أن تعرفنا في المقال السابق من سلسلة دليل جودو على كيفية ترتيب معالجة العقد والتنقل في شجرة المشاهد في محرك الألعاب جودو Godot، سنتعرف في هذا المقال على الطريقة السليمة للتواصل بين العقد Nodes ونوضح المشاكل التي قد تحدث عند القيام بممارسات غير متوافقة مع الطريقة الصحيحة المنصوح بها.
اقتباسملاحظة: اُستلهم المخطط الأصلي لهذا المقال من @TheDuriel على Godot Discord.
كما هو معروف، إذا أصبحت لدينا مشاهد ونسخ متعددة مع عدد كبير من العقد، فسيصبح مشروعنا معقدًا؛ وعندها قد نكتب شيفرة برمجية تشبه ما يلي:
get_node("../../SomeNode/SomeOtherNode") get_parent().get_parent().get_node("SomeNode") get_tree().get_root().get_node("SomeNode/SomeOtherNode")
وهنا إذا كتبنا الكود بهذا الشكل، فسنجد أن مثل مراجع References هذه العقد ستنكسر بكل سهولة، بحيث قد يتسبب إحداث تغيير بسيط في شجرة المشاهد في جعل مراجع العقد غير صالحة؛ ولهذا السبب، لا يجب أن يكون الاتصال بين العقد والمشاهد معقدًا.
يجب أن تدير العقد أبناءها وليس العكس؛ حيث إذا استخدمنا الدالة get_parent()
أو get_node("..")
، فيُحتمَل أننا نتجه إلى مشكلة ما، إذ تكون مثل هذه المسارات للعقد سهلة الكسر. سنذكر ففيما يلي المشاكل الرئيسية الثلاث في هذا الترتيب:
-
لا يمكن اختبار مشهد بطريقة مستقلة؛ فإذا شغّلنا المشهد بمفرده أو في مشهد اختبار لا يحتوي على إعداد العقدة نفسه، فستسبّب الدالة
get_node()
عطلًا - لا يمكننا تغيير الأشياء بسهولة، لأننا إذا قرّرنا إعادة ترتيب أو تصميم الشجرة، فلن تكون المسارات صالحةً مجددًا
-
يبدأ ترتيب الجاهزية من الأبناء أولًا حتى الوصول إلى الأب أخيرًا، وهذا يعني فشل محاولة الوصول إلى خاصية الأب في التابع
_ready()
الخاصة بالعقدة، لأن الأب غير جاهزٍ بعد
ملاحظة: يُنصح بالاطلاع على المقال السابق للحصول على شرح لكيفية دخول العقد إلى الشجرة وكيف تصبح جاهزة.
لا بد من توفر إمكانية إنشاء نسخة لعقدة أو مشهد في أيّ مكان في اللعبة التي ننشؤها، مع عدم افتراض أيّ شيء حول ما سيكون عليه الأب. سنستخدم أمثلة مفصلة لاحقًا، ولكن سنتحدث الآن عن القاعدة الذهبية لتواصل العقد، والتي هي: "استدعاء للأسفل، وإشارة Signal للأعلى".
تشير هذه القاعدة إلى أنه في حال استدعَت العقدة ابنًا، بمعنى تنقلها إلى أسفل الشجرة، فسيكون استدعاء الدالة get_node()
مناسبًا؛ وإذا كانت العقدة بحاجة إلى التواصل مع أعلى الشجرة، فيجب استخدام إشارة لذلك. يؤدي استخدام هذه القاعدة عند تصميم إعداد المشهد الخاص بنا إلى الحصول على مشروع قابل للصيانة ومنظم جيدًا، وسنتجنب بذلك استخدام مسارات العقد المعقدة التي تؤدي إلى مشاكل.
لنلقِ الآن نظرةً على كلٍّ من هذه الاستراتيجيات مع بعض الأمثلة.
استخدام الدالة get_node()
يعبُر استدعاء الدالة get_node()
شجرة المشاهد باستخدام مسار معين للعثور على العقدة المسماة.
مثال عن استخدام get_node()
ليكن لدينا الضبط التالي:
يجب أن يُعلِم السكربت الموجود في العقدة Player
العقدةَ AnimatedSprite2D
بالرسوم المتحركة التي تُشغَّل بناءً على حركة اللاعب، بحيث تعمل الدالة get_node()
في هذه الحالة بنجاح كما يلي:
extends CharacterBody2D func _process(delta): if speed > 0: get_node("AnimatedSprite2D").play("run") else: get_node("AnimatedSprite2D").play("idle")
ملاحظة: يمكننا استخدام المحرف $
في لغة GDScript كاختصار لاستدعاء get_node()
، لذا بإمكاننا كتابة $AnimatedSprite2D
مباشرةً.
طريقة أفضل للاستدعاء
تتمثل سلبية الطريقة السابقة في تحديد مسار العقدة، حيث إذا تغير هذا المسار لاحقًا، فيجب تعديل الشيفرة البرمجية أيضًا؛ ولهذا يمكننا استخدام الميزة @export
لتحديد عقدة مباشرةً كما يلي:
extends CharacterBody2D @export var animation : AnimatedSprite2D func _process(delta): if speed > 0: animation.play("run") else: animation.play("idle")
يمكننا باستخدام هذه الطريقة إسناد قيمةٍ إلى المتغير مباشرةً في الفاحص Inspector من خلال اختيار العقدة.
استخدام الإشارات Signals
يجب استخدام الإشارات لاستدعاء الدوال في العقد الموجودة في مستوًى أعلى من الشجرة أو الموجودة على المستوى نفسه (أي العقد الأشقاء Siblings).
يمكننا توصيل إشارة في المحرّر للعقد الموجودة قبل بدء اللعبة في أغلب الأحيان أو في الشيفرة البرمجية للعقد التي تنشِئ نسخةً منها في وقت التشغيل، وتكون صيغة توصيل الإشارة كما يلي:
signal_name.connect(target_node.target_function)
عند تجربة الاتصال بعقدة شقيقة، قد يبدو أننا سنحتاج إلى مسارات للعقدة، مثل المسار ../Sibling
، ولكن كما نرى، ذلك يخالف القاعدة السابقة. ولتفادي أي خلط، لا بد من التأكد دائمًا من أن الأب المشترك هو الذي يجري الاتصالات، نظرًا لتمكن عقدة الأب التي تُعَد أبًا مشتركًا لعقدتي الإشارة والاستقبال من تحديد مكانهما وأنها ستكون جاهزة بعدهما باتباع قاعدة الاستدعاء في الشجرة من الأعلى للأسفل.
مثال عن استخدام الإشارات
يُعَد تحديث واجهة المستخدم UI الخاصة بك حالة استخدام شائعة جدًا للإشارات، لأننا نريد مثلًا تحديث عرض Label
أو ProgressBar
كلما اختلف المتغير health
الخاص باللاعب، ولكن تكون عقد واجهة المستخدم الخاصة بك منفصلة تمامًا عن اللاعب إذ لا يعرف اللاعب شيئًا عن مكان هذه العقد وكيفية العثور عليها.
ليكن لدينا إعداد المثال التالي:
يمكننا ملاحظة أن واجهة المستخدم هي نسخة من مشهد، لأننا نعرض العقد المضمَّنة فقط، وهو المكان الذي نرى فيه أشياء مثل get_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health)
التي نريد تجنبها، لذا يصدر اللاعب بدلًا من ذلك إشارة health_changed
كلما أضاف أو فقد جزءًا من صحته، ويجب إرسال هذه الإشارة إلى الدالة update_health()
الخاصة بواجهة المستخدم UI، والتي تتولّى ضبط قيمة Label
.
سنستخدم الشيفرة البرمجية التالية في سكربت Player
كلما تغيرت صحة اللاعب:
health_changed.emit(health)
لدينا ما يلي في سكربت واجهة المستخدم UI
:
onready var label = $VBoxContainer/HBoxContainer/Label func update_health(value): label.text = str(value)
كل ما نحتاجه الآن هو توصيل الإشارة بالدالة، والمكان المثالي ذلك موجود في سكربت World
الذي يمثّل الأب المشترك للعقدتين ويعرف مكانهما:
func _ready(): $Player.health_changed.connect($UI.update_health)
استخدام المجموعات
تُعَد المجموعات طريقةً أخرى لفك الارتباط بين عقدتين، وخاصةً عندما تكون لدينا الكثير من الكائنات المتشابهة التي تطبّق الشيء نفسه؛ إذ يمكن إضافة عقدة إلى أيّ عددٍ من المجموعات، كما يمكن تغيير العضوية ديناميكيًا في أيّ وقت باستخدام الدالتين add_to_group()
و remove_from_group()
.
من المفاهيم الخاطئة الشائعة حول المجموعات أنها نوع من الكائنات أو المصفوفات التي تحتوي على مراجعٍ للعقد، ولكن المجموعات هي نظام وسم Tagging System، في حين تكون العقدة ضمن مجموعة عند إسناد وسمٍ من هذه المجموعة إليها.
تتعقّب شجرة المشهد SceneTree الوسوم ويكون لديها دوال مثل الدالة get_nodes_in_group()
للمساعدة في العثور على جميع العقد التي يكون لها وسم معين.
مثال عن استخدام المجموعات
لتكن لدينا لعبة إطلاق نار فضائية مثل لعبة غالاغا Galaga، مع وجود الكثير من الأعداء الذين يطيرون حول الشخصية الرئيسية، وقد يكون للأعداء أنواع وسلوكيات مختلفة، ونريد إضافة ترقية للقنبلة الذكية Smart Bomb التي تدمّر جميع الأعداء على الشاشة عند تنشيطها. هنا يمكننا الأمر باستخدام المجموعات بأقل قدر من الشيفرة البرمجية.
سنحتاج أولًا إلى إضافة جميع الأعداء إلى المجموعة enemies في المحرّر باستخدام تبويب العقدة Node:
يمكننا أيضًا إضافة عقد إلى المجموعة في السكربت الخاص بنا كما يلي:
func _ready(): add_to_group("enemies")
لنفترض أن لكل عدو الدالة explode()
التي تتعامل مع ما يحدث عندما يموت، مثل تشغيل رسوم متحركة أو توليد عناصر متساقطة وما إلى ذلك. يمكننا الآن تنفيذ دالة القنبلة الذكية smart bomb الخاصة بنا كما يلي بعد أن أصبح كل عدو في مجموعته:
func activate_smart_bomb(): get_tree().call_group("enemies", "explode")
استخدام خاصية المالك owner
owner
هي خاصية عقدة Node
التي تُضبَط تلقائيًا عند حفظ مشهد. تُضبَط هذه الخاصية لكل عقدة في هذا المشهد على العقدة الجذر للمشهد، مما يوفر طريقةً ملائمةً لتوصيل إشارات العقد الأبناء بالعقدة الرئيسية.
مثال عن استخدام الخاصية owner
سنحصل في أغلب الأحيان على تسلسل هرمي عميق ومتداخل من الحاويات وعناصر التحكم في واجهة مستخدم معقدة، وتصدر العقد التي يتفاعل معها المستخدم مثل عقدة Button
إشاراتٍ. قد نرغب في ربط هذه الإشارات بالسكربت الموجود على عقدة الجذر لواجهة المستخدم.
ليكن لدينا مثلًا الإعداد التالي:
يحتوي السكربت الموجود على العقدة الجذر CenterContainer
على الدالة التالية التي نريد استدعاءها عند الضغط على أيّ زر:
extends CenterContainer func _on_button_pressed(button_name): print(button_name, " was pressed")
تُعَد الأزرار هنا نسخًا من مشهد Button
، وتمثّل كائنًا قد يحتوي على شيفرة برمجية ديناميكية تضبط نص الزر أو خاصيات أخرى؛ أو قد تكون لدينا أزرار تُضاف أو تُزال ديناميكيًا من الحاوية وفقًا لحالة اللعبة، ولكن ما نحتاجه لتوصيل إشارة الزر هو ما يلي:
extends Button func _ready(): pressed.connect(owner._on_button_pressed.bind(name))
وكما هو ظاهر، ستبقى العقدة CenterContainer
هي المالك owner
بغض النظر عن المكان الذي تضع فيه الأزرار في الشجرة إذا أضفتَ مزيدًا من الحاويات مثلًا.
ختامًا
بهذا نكون قد تعرفنا على كيفية تحقيق تواصل سليم بين العقد Nodes في محرك الألعاب جودو Godot، وسنتابع في المقال التالي من هذه السلسلة في شرح مفهوم جديد من عالم الألعاب وهو delta.
ترجمة -وبتصرّف- للقسم Node communication من توثيقات Kidscancode.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.