تنظيم المشاهد في جودو

من موسوعة حسوب

سنغطّي في هذا القسم جميع المواضيع المرتبطة بالتنظيم الجيد لمحتوى المشهد، وسنجيب على بعض الأسئلة؛ ما هي العقد التي يجب أن تستخدمها؟ كيف عليك ترتيبها؟ كيف يجب على العقد أن تتفاعل مع بعضها البعض؟

‎‎كيفية بناء العلاقات بطريقة مثلى

يواجه مستخدمو جودو المشاكل التالية عندما يبدؤون بإنشاء المشاهد في مشاريعهم:

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

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

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

الطريقة المثلى لتصميم المشاهد هي بتصميمها دون احتوائها على أي اعتماديات إذا أمكن، أي يجب على المشهد أن يحتوي كل شيء يستخدمه بداخله. إذا كان لا بدّ من المشهد أن يتفاعل مع كائنات أخرى خارج سياقه فالحلّ الذي ينصح به المطوّرون المتمرّسون يكمن بحقن الاعتمادية dependency injection، إذ تنضوي هذه الطريقة على وجود واجهة برمجية API عالية المستوى تقدّم الاعتماديات للواجهة البرمجية ذات المستوى الأدنى؛ لكن لم قد تفعل ذلك؟ لأنه يمكن للأصناف التي تعتمد على بيئتها الخارجية توليد أخطاء وسلوك غير متوقّع.

لفعل ذلك، عليك كشف البيانات التي تريد مشاركتها وتعتمد على كائن أب لتهيئتها:

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

# الأب
$Child.signal_name.connect(method_on_the_object)

# الابن
signal_name.emit() # يشغل السلوك المعرف في الأب

ثانيًا: استدعاء تابع للبدء بالسلوك.

# الأب
$Child.method_name = "do"

# الابن بافتراض احتوائه على خاصية‪ "method_name" من نوع سلسلة نصية وتابع "do"
call(method_name) # استدعاء التابع المعرف في الأب (الذي يجب على الابن أن يمتلكه).

ثالثًا: تهيئة خاصية قابلة للاستدعاء callable، وهي عملية أكثر أمانًا من وجود تابع لأن بما أن ملكية التابع غير مهمة، ومن ثم استخدامها للبدء بالسلوك.

# الأب
$Child.func_property = object_with_method.method_on_the_object

# الابن
func_property.call() # استدعاء الدالة المعرفة في الأب (يمكن الاستدعاء من أي مكان).

رابعًا: تهيئة عقدة أو أي مرجع لكائن آخر.

# الأب
$Child.target = self

# الابن
print(target) # استخدام عقدة معرفة في الأب

خامسًا: تهيئة NodePath.

# الأب
$Child.target_path = ".."

# الابن
get_node(target_path) # استخدام‫ NodePath معرفة في الأب

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

# أب
$Left.target = $Right.get_node("Receiver")

# شقيق أيسر
var target: Node
func execute():
    # بعض العمليات على‫ target

# شقيق أيمن
func _init():
    var receiver = Receiver.new()
    add_child(receiver)

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

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

لتجنب عملية إنشاء توثيق مشابه والمحافظة عليه يمكن أن تحوّل العقدة التي تعتمد على اعتمادية ما (العقدة الابن في مثالنا السابق) إلى أداة سكربت tool script تطبق get_configuration_warnings()‎، الذي يُعيد بدوره مصفوفة غير فارغة من النوع PackedStringArray تُساهم في جعل نافذة المشاهد تعرض أيقونة تحذير مصحوبًة بسلاسل نصية بجانب العقدة، وهي الأيقونة ذاتها التي تظهر للعقد الأخرى مثل Area2D عندما لا تحتوي على عقدة ابن CollisionShape2D معرّفة، إذ أن المحرر سيوثق المشهد بنفسه عن طريق شيفرة المشهد البرمجية، ولا حاجة لتكرار المحتوى في التوثيق.

تساعد الواجهة الرسومية هذه بإعلام المستخدمين بمعلومات هامة بخصوص عقدة ما؛ هل تحتوي على اعتماديات خارجية؟ هل هذه الاعتماديات متاحة؟ سيحتاج المبرمجون الآخرون وخصوصًا الكتّاب والمصممون إلى تعليمات واضحة في الرسائل التي تخبرهم بما يجب عليهم ضبطه.

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

يجب أن تلتزم السكربتات والمشاهد بجميع مبادئ البرمجة كائنية التوجه بالنظر إلى أنها امتداد من أصناف المحرّك بذاته؛ بعض المبادئ تتضمّن:

  • SOLID
  • DRY
  • KISS
  • YAGNI

اختيار هيكل لشجرة العقد

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

يجب على أي لعبة أن تحتوي على نقطة بداية، حيث يمكن للمطور بدءً منها أن يتتبع منطق اللعبة بدءً من تلك النقطة، تساعد هذه النقطة أيضًا بكونها نقطة مركزية يمكنك منها الاطّلاع على جميع البيانات والمنطق الموجود في البرنامج. تدعى هذه النقطة في التطبيقات الاعتيادية عادةً بدالة main، وفي هذه الحالة فهي العقدة Main.

  • العقدة الأساسية (main.gd)

يمثّل السكربت main.gd المتحكم الأساسي باللعبة، وعندما تنشئ عالم اللعبة World (سواءً أكان ثنائي الأبعاد أو ثلاثي الأبعاد) فسيكون ابنًا للعقدة Main. بالإضافة لذلك، ستحتاج لواجهة رسومية GUI أساسية للعبة التي ستُدير القوائم والمكونات المتنوعة التي سيحتاجها المشروع.

أي تصبح الهيكلية على الشكل التالي:

  • العقدة الأساسية Main واسمها main.gd
    • عقدة العالم Node2D/Node3D واسمها game_world.gd
    • عقدة الواجهة الرسومية GUI واسمها gui.gd

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

تكمن الخطوة الثانية بالتفكير بمتطلبات نظام اللعب إذا كان النظام:

  1. يتتبع جميع بياناته داخليًا
  2. يمكن الوصول إلى البيانات بشكل عام globally
  3. البيانات موجودة في عزلة

ومن ثم يمكنك إنشاء عقدة للضبط المبدئي وحيدة المسؤولية singleton autoload node.

يمكن استخدام بديل أبسط بتحكم أقل للألعاب الأصغر حجمًا بوجود عقدة لعبة Game وحيدة المسؤولية تستدعي التابع SceneTree.change_scene_to_file()‎ لتبديل محتوى المشهد، ويحافظ هذا الهيكل على عقدة العالم بكونها عقدة اللعبة الأساسية.

يجب على أي واجهة رسومية أن تكون وحيدة المسؤولية أيضًا؛ سواءً بكونها جزءً انتقاليًا من عقدة العالم أو بإضافتها يدويًا كابن مباشر للعقدة الجذر، وإلا فستحذف عقد الواجهات الرسومية نفسها خلال عملية تبديل المشاهد.

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

كل نظام جزئي في اللعبة يجب أن يحتوي على قسم خاص به بداخل شجرة المشاهد SceneTree، ويجب استخدام علاقات عقدة ابن-اب فقط في الحالات التي يكون فيها العقد عناصر فعالة من آبائها، لكن هل هذا يعني أنه يجب عليك إزالة العقد الابن إذا أزلت العقدة الأب؟ إذا لم تفعل ذلك، فيجب أن تمتلك مكانها الخاص في الهيكلية بكونها عقد أشقاء أو أي علاقة أخرى.

نحتاج أيضًا في بعض الحالات أن تموضع العقد بين بعضها نسبيًا، وهنا يمكنك استخدام العقد RemoteTransform/RemoteTransform2D، إذ ستسمح للعقدة الهدف بوراثة عناصر التحويل المحدّدة من العقدة Remote*‎، ولإسناد العقدة target من نوع NodePath استخدم طريقة من الطرق التالية:

  1. كائن طرف ثالث مثل عقدة أب، تحلّ محل الوسيط وتسند المهمة.
  2. مجموعة للحصول على مرجع بسهولة للعقدة المطلوبة (بفرض أنه يوجد فقط هدف واحد من الأهداف).

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

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

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

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

  1. توافقية وثبات أكبر.
  2. انعدام وجود الحالات المميزة التي يجب عليك توثيقها وتنظيمها في مكان ما.
  3. لا يوجد مساحة للأخطاء لأن هذه التفاصيل غير مهمة.

على الجانب الآخر، إذا أردت وجود عقدة ابن لا ترث خاصية التحول transform من أبيها، فعليك القيام بالخطوات التالية:

  1. الحل الصريح: ضع عقدة بينهما، بما أن العقدة لا تحتوي على خاصية التحول فلن تمرّر الخاصية هذه إلى العقدة الابن.
  2. الحل الأفضل: استخدام خاصية top_level للعقدة CanvasItem أو Node3D، مما سيجعل من العقدة تتجاهل خاصية التحول التي ورثتها.

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

الأمر الأساسي في تنظيم المشاهد يكمن بالنظر إلى شجرة المشاهد SceneTree بسياق نسبي بدلًا من سياق مكاني؛ هل تعتمد العقد على وجود العقد الأب الخاصة بها؟ إذا كان الجواب لا، فهذا يعني أنها ستعمل بنجاح في أي مكان، وإذا كان الجواب بنعم، فهذا يعني أن العقد يجب أن تكون عقد ابن للعقدة الأب على الدوام (وجزء من مشهد العقدة الأب غالبًا).

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

مصادر