واجهات جودو
ستحتاج في بعض الأحيان للسكربتات التي تعتمد على كائنات أخرى للميزات، هناك خطوتين لهذه العملية:
- الحصول على مرجع إلى الكائن الذي لديه تلك الخاصية
- الوصول للبيانات أو المنطق من ذلك الكائن
نتكلم هنا عن الطرق المتعددة للقيام بذلك.
الحصول على مرجع الكائن
أبسط طريقة للحصول على مرجع للكائنات هو الحصول على مرجع لكائن موجود مسبقًا من نسخة أخرى.
- بلغة GDScript:
var obj = node.object # وصول إلى خاصية
var obj = node.get_object() # وصول إلى تابع
- بلغة C#:
GodotObject obj = node.Object; // وصول إلى خاصية
GodotObject obj = node.GetObject(); // وصول إلى تابع
يمكن تطبيق هذا المبدأ من أجل كائنات RefCounted، بالرغم من أن المستخدمين يصلون إلى العقد Node والموارد Resource بهذه الطريقة إلا أنه يوجد أيضًا طرق أخرى.
عوضًا عن الوصول عن طريق الخاصية أو التابع، يمكن الوصول إلى الموارد عن طريق وصول التحميل load access.
- بلغة GDScript:
# إذا احتجت إلى export const var (وهو أمر غير موجود)، فيمكنك استخدام ضابط لسكربت يعمل كأداة للتحقق
# ما إذا كان يُنفّذ في المحرر
# يجب أن تُكتب '@tool' في بداية السكربت
@tool
# تحميل الموارد عند تحميل المشهد
var preres = preload(path)
# تحميل الموارد عندما يصل البرنامج إلى التعليمة
var res = load(path)
# لاحظ أن المستخدمين يحملون المَشاهد والسكربتات باستخدم حالة أحرف باسكال اصطلاحًا
# باستخدام اسماء مثل أسماء الأنواع وبكونها ثوابت
const MyScene = preload("my_scene.tscn") # تحميل ساكن
const MyScript = preload("my_script.gd")
# يختلف الأمر بالنسبة لقيمة النوع هذا أي أنه متغير، وبالتالي يستخدم نمط الثعبان
@export var script_type: Script
# يجب ضبطها من المحرر، وإلا فإنها تصبح null بشكل افتراضي
export var const_script: Script:
set(value):
if Engine.is_editor_hint():
const_script = value
# تحذير المستخدمين أن القيمة لم تُضبط بعد
func _get_configuration_warnings():
if not const_script:
return ["Must initialize property 'const_script'."]
return []
- بلغة C#:
// سكربت أداة أضيف لأجل مثال const [Export]
[Tool]
public MyType
{
// تحميل ضبط الخاصية الأولي عند إنشاء نسخة من السكربت أي استخدام new()
// لا يوجد أي تحميل مُسبق خلال تحميل المشهد في سي شارب
// ضبط أولي بوجود قيمة يمكن تعديلها في وقت التنفيذ
public Script MyScript = GD.Load<Script>("MyScript.cs");
// ضبط أولي بقيمة مماثلة، إلا أنه لا يمكن تعديلها
public readonly Script MyConstScript = GD.Load<Script>("MyScript.cs");
// للقراءة فقط بسبب ضابط لا يمكن الوصول إليه
// لكن يمكن ضبط القيمة خلال إنشائها باستخدام الباني myType()
public Script Library { get; } = GD.Load<Script>("res://addons/plugin/library.gd");
// إذا احتجت إلى const [Export] الغير متوفرة، فيمكنك استخدام ضابط شرطي لأداة السكربت الذي يتحقق من تنفيذ المحرر
private PackedScene _enemyScn;
[Export]
public PackedScene EnemyScn
{
get { return _enemyScn; }
set
{
if (Engine.IsEditorHint())
{
_enemyScn = value;
}
}
};
// تحذير المستخدمين أن القيمة لم تُضبط
public String _GetConfigurationWarnings()
{
if (EnemyScn == null)
return "Must initialize property 'EnemyScn'.";
return "";
}
}
لاحظ التالي:
- هناك عدة طرق تحمّل فيها اللغة البرمجية هذه الموارد
- عند تصميم كيفية وصول الكائن للبيانات، لا تنسى أنه يمكن تمرير الموارد كمراجع أيضًا
- انتبه أن تحميل الموارد يجلب الموارد المخزنة مؤقتًا والمحافظ عليها من المحرك. يجب نسخ المرجع الموجود للحصول على كائن جديد أو نسخ كائن من الأول باستخدام
new()
لدى العقد أيضًا نقطة وصول أخرى ألا وهي شجرة المَشاهد SceneTree
.
- لغة GDScript
extends Node
# الطريقة البطيئة
func dynamic_lookup_with_dynamic_nodepath():
print(get_node("Child"))
# الطريقة السريعة المتاحة في جي دي سكربت فقط
func dynamic_lookup_with_cached_nodepath():
print($Child)
# الطريقة الأسرع التي لا تتوقف عن العمل إذا تحركت العقدة فيما بعد
# لاحظ أن @onready موجودة في جي دي سكربت فقط
# إذ عليك القيام بالتالي في اللغات الأخرى
# var child
# func _ready():
# child = get_node("Child")
@onready var child = $Child
func lookup_and_cache_for_future_access():
print(child)
# الطريقة الأسرع، لا تتوقف عن العمل إذا تحركت العقدة في شجرة المَشاهد
# يجب أن تُحدَّد العقدة في المحرر بالنظر إلى أنها خاصية مستوردة
@export var child: Node
func lookup_and_cache_for_future_access():
print(child)
# تفويض مهمة إسناد المرجع إلى مصدر خارجي
# السيئة: تُجري تحققًا
# الحسنة: لا تتطلب العقدة أي شيء من هيكلها الخارجي
# يمكن أن تأتي prop من أي مكان
var prop
func call_me_after_prop_is_initialized_by_parent():
# يمكن التحقق من prop بثلاث طرق
# الفشل دون تنبيه
if not prop:
return
# الفشل مع رسالة خطأ
if not prop:
printerr("'prop' wasn't initialized")
return
# الفشل مع إنهاء البرنامج
# ملاحظة: السكربتات التي تُنفّذ من قالب تصدير لا تنفذ توكيدات
assert(prop, "'prop' wasn't initialized")
# استخدام تحميل تلقائي
# هذه العملية خطيرة للعقد الاعتيادية إلا أنها مفيدة للعقد المتفردة
# التي تُدير بياناتها بنفسها ولا تتدخل مع الكائنات الأخرى
func reference_a_global_autoloaded_variable():
print(globals)
print(globals.prop)
print(globals.my_getter())
- لغة C#
using Godot;
using System;
using System.Diagnostics;
public class MyNode : Node
{
// الطريقة البطيئة
public void DynamicLookupWithDynamicNodePath()
{
GD.Print(GetNode("Child"));
}
// الطريقة الأسرع، وهي البحث عن العقدة وتخزينها مؤقتًا للوصول المستقبلي
// لا تتسبب بمشاكل إذا تحركت العقدة لاحقًا
private Node _child;
public void _Ready()
{
_child = GetNode("Child");
}
public void LookupAndCacheForFutureAccess()
{
GD.Print(_child);
}
// تفويض إسناد المرجع إلى مصدر خارجي
// السيئة: بحاجة للقيام بتحقق
// الحسنة: لا تتطلب العقدة أي شيء من هيكلها الخارجي
// يمكن أن تأتي prop من أي مكان
public object Prop { get; set; }
public void CallMeAfterPropIsInitializedByParent()
{
// يمكن التحقق من prop بثلاث طرق
// الفشل دون تنبيه
if (prop == null)
{
return;
}
// الفشل مع رسالة خطأ
if (prop == null)
{
GD.PrintErr("'Prop' wasn't initialized");
return;
}
// الفشل مع استثناء
if (prop == null)
{
throw new InvalidOperationException("'Prop' wasn't initialized.");
}
// الفشل وإنهاء البرنامج
// ملاحظة: تعمل السكربتات من قالب تصدير، لا تنفذ Debug.Assert
Debug.Assert(Prop, "'Prop' wasn't initialized");
}
// استخدام تحميل تلقائي
// عملية خطيرة للعقد العادية ومفيدة للعقد المتفردة التي تُدير بياناتها بنفسها دون التدخل مع كائنات أخرى
public void ReferenceAGlobalAutoloadedVariable()
{
MyNode globals = GetNode<MyNode>("/root/Globals");
GD.Print(globals);
GD.Print(globals.Prop);
GD.Print(globals.MyGetter());
}
};
الوصول للبيانات أو المنطق من كائن
إن واجهة برمجة التطبيقات في لغة جودو هي من نوع البطة Duck-typed هذا يعني إذا نفذ السكربت عملية فإن جودو لا تتحقق أنه يمكنها دعم هذه العملية عن طريق النوع، بل تتحقق من أن الكائن ينفذ التابع الفردي.
مثلًا، لدى صنف CanvasItem
خاصية visible
. إن كل الخاصيات المعرضة إلى واجهة برمجة التطبيقات للبرامج النصية هي بالحقيقة زوج ضابط setter وجالب getter مرتبطين باسم. إذا حاولت الوصول إلى CAnvasItem.visible
ستتحقق جودو من ذلك بهذه الخطوات بالترتيب:
- ستحاول ضبط الخاصية من خلال السكربت في حال كان لدى الكائن برنامج نصي مرتبط به، هذا سيجعل المجال مفتوحًا لتعيد السكربتات تعريف الخاصية المعرفة على الكائن الأساسي عن طريق إعادة تعريف تابع الضبط للخاصية
- ستبحث باستخدام HashMap في الصنف
ClassDB
في الخاصية "Visible" مع كل صنفCanvasItem
وكل الأنواع الموروثة في حال كان السكربت لا يوجد فيه خاصية وإذا وجدتها ستستدعي الجالب أو الضابط المرتبط فيه. لمعلومات أكثر عن HashMaps راجع مستند تفضيلات البيانات. - إذا لا يحتوي السكربت على الخاصية، تُجري بحثًا صريحًا لتعرف إذا ما كان المستخدم يريد الوصول إلى خاصيات "السكربت" أو الخاصيات "الوصفية".
- إذا احتوى على الخاصية، تتحقق من تطبيقات
_set
/_get
(حسب نوع الوصول) فيCanvasItem
وأنواعه الموروثة. يمكن أن تنفذ هذه التوابع منطقًا يلمح أن الكائن لديه خاصية، وهذه هي الحالة مع التابع _get_property_list
- يحصل ذلك حتى من أجل أسماء الرموز غير المسموح بها مثل أسماء تبدأ مع رقم أو تحتوي على خط مائل
لذلك يمكن لنظام نوع البطة تحديد الخاصية في النص البرمجي أي صنف الكائن أو أي صنف يرثه الكائن ولكن فقط لأمور تُضيف على الكائن.
تقدم جودوت مجموع من الخيارات لإجراء مجموعة من التحقيقات وقت التنفيذ على هذه الوصولات:
- وصول لخاصية من نوع البطة، ستكون هذه تحققات من خاصية (كما شُرح سابقا)، سيتوقف التنفيذ إذا لم تكن العملية مدعومة من الكائن
- بلغة GDScript:
# لجميع الكائنات من نوع البطة ضابط وجالب ومغلف للاستدعاء
get_parent().set("visible", false)
# استخدام رمز وصول بدلًا من سلسلة نصية في استدعاء التابع سيستدعي التابع set بشكل صريح
# مما سيؤدي إلى استدعاء تابع الضابط المرتبط بالخاصية عبر البحث عنها في السلسلة
get_parent().visible = false
# لاحظ أنه إذا كان هناك واحدة تعرّف _set وأخرى تعرّف _get وتصف وجود الخاصية
# لكن الخاصية لم يُتعرّف عليها بأي تابع _get_property_list، فهذا يعني أن التابعين set() وget() سيعملان
# لكن رمز الوصول سيزعم أنه لا يستطيع العثور على الخاصية
- بلغة C#:
// جميع الكائنات ذات نوع البطة تحتوي على ضابط وجالب ومغلف للاستدعاء GetParent().Set("visible", false); // سي شارب لغة برمجة ساكنة، لذا فهي لا تحتوي على رمز وصول ديناميكي مشابه لما يلي // `GetParent().Visible = false` هذا لن يعمل
- بلغة C#:
- تحقق تابع، في حالة
CanvasItem.visible
يمكن الوصول للتوابعset_visible
وis_visible
مثل أي تابع آخر- بلغة GDScript:
var child = get_child(0)
# بحث ديناميكي
child.call("set_visible", false)
# بحث ديناميكي مبني على الرمز
# تغيّر جي دي سكربت السطر التالي إلى استدعاء تابع خلف الكواليس
child.set_visible(false)
# بحث ديناميكي، يتحقق من تواجد التابع أولًا
if child.has_method("set_visible"):
child.set_visible(false)
# التحقق من تحويل الأنواع متبوعًا ببحث ديناميكي
# مفيد عندما تستدعي عدة استدعاءات آمنة، بحيث تعرف أن الصنف يطبّق جميعها
# وهنا لا حاجة للتحقق المتكرر
# يصبح الأمر صعبًاإذا نفذت تحقق من تحويل الأنواع لنوع معرف من قبل المستخدم، إذ أنها تجبر المزيد من الاعتماديات
if child is CanvasItem:
child.set_visible(false)
child.show_on_top = true
# اذا لم ترد أن تفشل هذه التحققات دون تنبيه المستخدمين فيمكنك استخدام التوكيدات بدلًا من ذلك
# إذ ستتسبب هذه بأخطاء وقت التنفيذ على الفور
assert(child.has_method("set_visible"))
assert(child.is_in_group("offer"))
assert(child is CanvasItem)
# يمكن استخدام تسميات الكائنات أيضًا للإشارة إلى واجهة (افتراض وجودها) تطبّق بعض التوابع
# هناك نوعان وكلاهما يوجد فقط للعقد وهما الأسماء والمجموعات
# افترض ما يلي
# كائن Quest يمكن نقله إلى حالة "مكتمل" أو "فشل"
# ويحتوي على نص قبل وبعد كل حالة
# 1. باستخدام اسم
var quest = $Quest
print(quest.text)
quest.complete() # أو quest.fail()
print(quest.text) # محتوى نصي جديد
# 2. باستخدام مجموعة
for a_child in get_children():
if a_child.is_in_group("quest"):
print(quest.text)
quest.complete() # أو quest.fail()
print(quest.text) # محتوى نصي جديد
# لاحظ أن هذه الواجهات مرتبطة باصطلاحات المشروع، إذ أن الفريق يعرّف السكربتات التي تتوافق مع توثيق الواجهة للاسم أو المجموعة التي ستُستخدم
- بلغة C#:
Node child = GetChild(0);
// بحث ديناميكي
child.Call("SetVisible", false);
// بحث ديناميكي، يتأكد من وجود التابع أولًا
if (child.HasMethod("SetVisible"))
{
child.Call("SetVisible", false);
}
// نستخدم مجموعة وكأنها واجهة، أي نفترض أنها تطبق بعض التوابع المعينة
// يتطلب الأمر توثيقًا جيدًا للمشروع لإبقاء هذا الأمر موثوقًا (إلا إن كنت تُنشئ أدوات محرر لتُجبرها وقت المحرر)
// هذا الأمر ليس جيدًا بقدر استخدام واجهة فعلية في سي شارب، إلا أنه لا يمكننا ضبط واجهو سي شارب من المحرر نظرًا إلى أنها ميزات لغة عالية المستوى
if (child.IsInGroup("Offer"))
{
child.Call("Accept");
child.Call("Reject");
}
// التحقق من تحويل الأنواع متبوعًا ببحث ساكن
CanvasItem ci = GetParent() as CanvasItem;
if (ci != null)
{
ci.SetVisible(false);
// مفيد عندما تريد أن تُجري عدة استدعاءات آمنة على الصنف
ci.ShowOnTop = true;
}
// إذا لم ترد لهذه التحققات أن تفشل دون تنبيه المستخدمين فيمكنك استخدام توكيد بدلًا منها
// وهذه ستتسبب بأخطاء وقت التنفيذ على الفور
Debug.Assert(child.HasMethod("set_visible"));
Debug.Assert(child.IsInGroup("offer"));
Debug.Assert(CanvasItem.InstanceHas(child));
// يمكن أيضًا استخدام تسميات الكائن للإشارة إلى وجود واجهة (افتراض أنها تطبّق بعض التوابع)
// هناك نوعان لذلك وكلاهما يُستخدمان فقط مع العقد وهما الأسماء والمجموعات
// بافتراض ما يلي:
// وجود كائن Quest يحتوي على حالة Complete أو Fail
// ويحتوي على نص متاح قبل وبعد كل حالة
// 1. باستخدام اسم
Node quest = GetNode("Quest");
GD.Print(quest.Get("Text"));
quest.Call("Complete"); // أو "Fail"
GD.Print(quest.Get("Text")); // محتوى نصي جديد
// 2. باستخدام مجموعة
foreach (Node AChild in GetChildren())
{
if (AChild.IsInGroup("quest"))
{
GD.Print(quest.Get("Text"));
quest.Call("Complete"); // أو "Fail"
GD.Print(quest.Get("Text")); // محتوى نصي جديد
}
}
// لاحظ أن هذه الواجهات مخصصة بحسب كل اصطلاحات مشروع وتعريف الفريق لها
// يجب أن يتوافق كل سكربت مع الواجهة الموثقة من حيث الاسم أو المجموعة التي تملؤها
// كما أن هذه التوابع في سي شارب ستكون أبطأ من الوصول الساكن باستخدام الواجهات التقليدية
- إسناد الوصول إلى Callable، يمكن لذلك أن يكون مفيدًا في الحالات التي نحتاج فيها لأقصى درجات الحرية من الاعتماديات. في هذه الحالة سنعتمد على سياق خارجي لضبط التابع
- بلغة GDScript:
# child.gd
extends Node
var fn = null
func my_method():
if fn:
fn.call()
# parent.gd
extends Node
@onready var child = $Child
func _ready():
child.fn = print_me
child.my_method()
func print_me():
print(name)
- بلغة C#:
// Child.cs
using Godot;
public partial class Child : Node
{
public Callable? Callable { get; set; }
public void MyMethod()
{
Callable?.Call();
}
}
// Parent.cs
using Godot;
public partial class Parent : Node
{
private Child _child;
public void _Ready()
{
_child = GetNode<Child>("Child");
_child.Callable = Callable.From(PrintMe);
_child.MyMethod();
}
public void PrintMe() {
{
GD.Print(Name);
}
}
تساهم هذه الاستراتيجيات بتصميم جودو المرن، إذ يكون لدى المستخدمين العديد من الأدوات للوصول إلى غايتهم المحددة.