الفرق بين المراجعتين لصفحة: «Godot/best practices/logic preferences»

من موسوعة حسوب
Naser-dakhel (نقاش | مساهمات)
طلا ملخص تعديل
Naser-dakhel (نقاش | مساهمات)
 
(لا فرق)

المراجعة الحالية بتاريخ 20:37، 2 سبتمبر 2023


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

إضافة العقد وتغيير الخصائص: أيهما أولًا؟

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

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

من الأفضل دائمًا ضبط القيم الأولية للعقدة قبل إضافتها إلى شجرة المَشاهد.

التحميل أم التحميل المسبق

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

بينما يحمّل التابع الذي يقابله المورد عندما يصل إلى تعليمة التحميل، وهذا يعني أن المورد سيُحمّل عند الحاجة إليه مما قد يسبب بطئًا عندما يحصل ذلك بالتقاطع مع عملية حساسة. الدالة load()‎ هي اسم بديل للتابع ResourceLoader.load(path) وهو بدوره تابع يمكن الوصول إليه في كل لغات البرمجة.

إذًا، متى نستخدم التحميل المُسبق ومتى نستخدم التحميل العادي، ومتى يجب ألّا نستخدم كلاهما؟ لننظر إلى المثال التالي:

بلغة جي دي سكربت:

# my_buildings.gd
extends Node

# لاحظ كيف أن السكربتات والعقد الثابتة لها اسم مختلف عن متغايرات الخصائص الخاصة بهما


# هذه القيمة هي ثابت، لذا فهي تُنشأ عندما يُحمّل كائن السكربت، ويحمّل السكربت مسبقًا القيمة
# الفائدة هنا هي أنه يمكن للمحرر أن يقدم ميزة الإكمال التلقائي، بما أن المسار يجب أن يكون مسار ساكن

const BuildingScn = preload("res://building.tscn")

# أولًا: يحمل السكربت القيمة مسبقًا بحيث يمكن تحميلها كاعتمادية للسكربت‫ my_buildings.gd 
#    لكن وبما أنها خاصية وليست ثابت فإن الكائن لن ينسخ المورد‫ PackedScene المحمل مسبقًا إلى الخاصية
#    حتى يتم نسخ السكربت باستخدام‫ new()
#
# 2. لا يمكن الوصول للقيمة المحملة مسبقًا من كائن السكربت لوحده
#    بالتالي التحميل المسبق هنا لا يقدّم أي فائدة تُذكر
#
# 3. بما أن المستخدم يصدّر القيمة فإن شيفرة نسخ المشهد ستُعيد الكتابة على القيمة القيمة الأولية المخملة مسبقًا
# وذلك إذا كان السكربت المخزن في العقدة موجود في ملف مشهد، مما يتسبب بضياع القيمة
#    من الأفضل هنا تقديم قيمة فارغة أو غير قيم افتراضية غير صالحة للتصدير
#
# 4. يتم تحميل‫ office.tscn عندما يُنسخ هذا السكربت على نفسه باستخدام new()
#    بدلًا من القيمة المصدّرة
export(PackedScene) var a_building = preload("office.tscn")

# يتسبب هذا بخطأ‫!
# يجب علينا إسناد القيم الثابتة إلى ثوابت وذلك لأن‫ load تجري بحثًا عند وقت التنفيذ بطبيعتها
# وبالتالي لا يمكننا استخدامها لتهيئة ثابت
const OfficeScn = load("res://office.tscn")

# يتم التحميل بنجاح فقط عندما تنسخ السكربت
var office_scn = load("res://office.tscn")

بلغة سي شارب:

using Godot;

// لا يوجد في سي شارب ولغات البرمجة الأخرى مفهوم التحميل المسبق
public partial class MyBuildings : Node
{
    //هذا حقل يقبل القراءة فقط، ويمكن إسناده عند التصريح أو عند تنفيذ الباني
    public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");

    public PackedScene ABuilding;

    public override void _Ready()
    {
        // يمكن إسناد القيمة إليه عند التهيئة
        ABuilding = GD.Load<PackedScene>("res://office.tscn");
    }
}

يسمح التحميل المسبق للسكربت بالتعامل مع جميع عمليات التحميل عندما يتم تحميل السكربت، وهذا مفيد إلا أن هناك بعض الحالات التي لا يفضّل فيها استخدامه، ولتمييز هذه الحالات، هناك بعض الأشياء التي يجب أخذها بعين الاعتبار:

  1. إذا لم يكن من الممكن تحديد اللحظة التي يجب فيها تحميل السكربت، فتحميل المورد المسبق قد يؤدي بعمليات تحميل لا يُتوقّع حدوثها وخصوصًا إذا كان المورد مشهد أو سكربت، مما قد يؤدي إلى وقت تحميل متغير غير متوقّع يُضاف إلى وقت عمليات تحميل السكربت الأصلي.
  2. إذا كان من الممكن لشيء أن يستبدل القيمة (كتهيئة مشهد مصدّر)، فتحميل القيمة المسبق لا يقدم أي فائدة. هذه النقطة ليست بنقطة مهمة إذا ما كنت تنوي أن يُنشئ السكربت نفسه بنفسه.
  3. إذا كنت تريد استيراد مورد صنف آخر (مشهد أو سكربت) فاستخدام ثابت محمّل مسبقًا هو الخيار الأفضل غالبًا. إلا أن هناك حالات استثنائية تدفعك بعيدًا عن القيام بذلك:
    1. إذا كان من الممكن للصنف المستورد أي يتغيّر، في هذه الحالة يجب أن يكون خاصية وأن يتم تهيئته باستخدام export أو load(‎) (لربما أيضًا يجب تفادي تهيئته إلى أن تحتاج إليه لاحقًا).
    2. إذا كان السكربت يتطلب الكثير من الاعتماديات، ولم تكن تريد استهلاك الكثير من الذاكرة، ففي هذه الحالة الخيار الأفضل هو تحميل وإزالة تحميل الاعتماديات المختلفة وقت التنفيذ مع تغير الظروف، إذا حملت الموارد مسبقًا إلى ثوابت فالطريقة الوحيدة لإزالة تحميلها تكمن بإعادة تحميل السكربت كاملًا، مقارنةً بالطريقة الأخرى؛ إذا تم تحميلها إلى خصائص فيمكن ضبطها إلى null وإزالة المراجع إلى المورد بشكل كامل (وهو نوع مستمدّ من RefCounted، الذي سيتسبب بحذف الموارد من الذاكرة).

المراحل الكبيرة: ساكنة أم ديناميكية

أيّ الحالات أكثر ملائمة إذا أنشأت مرحلة كبيرة؟ هل عليك إنشاء المرحلة كمساحة ساكنة واحدة؟ أم عليك تحميل المرحلة كأجزاء وتغيير محتوى العالم وفق التغييرات؟

الإجابة البسيطة لذلك السؤال هي "اتبّع الحالة التي تناسب الأداء"، المعضلة المرتبطة بهذين الخيارين هي معضلة قديمة يفكّر بها المبرمجون على الدوام: هل أحسّن الذاكرة على حساب السرعة أم بالعكس؟

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

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

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

وبناءً على ما سبق، فإن أفضل الخيارات هي:

  1. استخدام مراحل ساكنة للألعاب صغيرة الحجم.
  2. أنشئ مكتبة أو إضافة تبرمج إدارة العقد والمَشاهد إذا كنت تمتلك الوقت والموارد عند العمل على لعبة متوسطة إلى كبيرة الحجم، وإذا تم تحسينها على مدار الوقت سيؤدي ذلك إلى تحسين الاستخدام والاستقرار ومن ثم استخدامها على صعيد عدة مشاريع أخرى.
  3. برمِج المنطق الديناميكي للعبة متوسطة إلى كبيرة الحجم إذا امتلكت المهارات اللازمة، ولكن لم تمتلك الوقت أو الموارد لتحسين الشيفرة البرمجية (عليك إنهاء العمل على اللعبة في الوقت بنهاية المطاف!) ومن الممكن العمل على إعادة بناء الشيفرة البرمجية لاستخدام هذه الشيفرة البرمجية ضمن إضافة.

يمكنك الاطّلاع على "تغيير المشاهد يدويًا" إذا أردت مثالًا على الطرق المختلفة التي يمكنك بها تبديل المَشاهد عند وقت التنفيذ.

مصادر