Roblox Guidebookقاعدة المعرفة
العربية ⌄

التطوير / ROBLOX

اتصالات أحداث Roblox: لا تضف مستمعاً آخر كلما فتحت القائمة

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

آخر تحديث:

اختر آلية خطأ واحدة للتحقيق #

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

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

افصل الحدث عن اتصاله #

تصف الوثائق الرسمية Connect كطريقة لربط دالة بحدث، وتعيد كائناً من نوع RBXScriptConnection. الاحتفاظ بهذا الكائن يسمح للمالك بفصل الاشتراك المحدد عبر Disconnect. الإشارة ودالة callback وكائن الاتصال المرجع ثلاثة أشياء مختلفة. المتغير الذي يحتوي الدالة نفسها ليس تلقائياً مرجعاً لكائن الاتصال الذي أنشأته، ولذلك يجب حفظ النتيجة المناسبة.

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

اشتراك واحد له مالكافتح الصورة بالحجم الكامل ↗
مخطط أصلي لملكية الاشتراك.

أعد إنتاج ثلاثة مستمعين غير مطلوبين #

ينشئ الجزء الأول من الاختبار الأصلي ثلاثة اتصالات مع BindableEvent واحدة. يزيد كل callback العداد المشترك hits. بعد ذلك يُطلق الحدث مرة واحدة. بعد انتظار المعالجة يجب أن يصبح hits ثلاثة. يؤكد هذا ثلاث استجابات لثلاثة اشتراكات نشطة في الحالة المعزولة، ولا يدّعي إعادة إنتاج زر معطّل في تجربة موجودة بعينها أو قياس عدد نقرات لاعب حقيقي.

احتفظ بكل كائن اتصال مُعاد في جدول duplicates حتى تفصل جميع المستمعين الذين أنشأتهم المقارنة لاحقاً. يتيح هذا الأساس تكرار الفرق بين التصميم غير المُدار والتصميم المُدار. لا تسمِّ عدد الاشتراكات عدد النقرات. لدينا إطلاق واحد للحدث وثلاث دوال مرتبطة بصورة مستقلة؛وهما عدّادان مختلفان يجيبان عن سؤالين مختلفين حول النتيجة التي ظهرت.

-- Original isolated engine experiment, not an existing-game script.
local signalOwner = Instance.new("BindableEvent")
local hits = 0
local duplicates = {}
for i = 1, 3 do
    duplicates[i] = signalOwner.Event:Connect(function()
        hits += 1
    end)
end
signalOwner:Fire()
task.wait()
assert(hits == 3, "Three live subscriptions must make three callbacks")
for _, connection in duplicates do
    connection:Disconnect()
    assert(connection.Connected == false)
end
signalOwner:Fire()
task.wait()
assert(hits == 3, "Disconnected listeners must not receive a new fire")

local currentConnection
local function closePanel()
    if currentConnection then
        currentConnection:Disconnect()
        currentConnection = nil
    end
end
local function openPanel()
    closePanel()
    currentConnection = signalOwner.Event:Connect(function()
        hits += 1
    end)
end
openPanel()
openPanel()
openPanel()
signalOwner:Fire()
task.wait()
assert(hits == 4, "Reopening must leave only one listener")
closePanel()
closePanel()
signalOwner:Fire()
task.wait()
assert(hits == 4, "Repeated cleanup must be safe and stop new callbacks")
openPanel()
signalOwner:Fire()
task.wait()
assert(hits == 5, "A fresh panel must work after cleanup")
closePanel()

local onceHits = 0
local onceConnection = signalOwner.Event:Once(function()
    onceHits += 1
end)
signalOwner:Fire()
signalOwner:Fire()
task.wait()
assert(onceHits == 1, "Once must handle only the first invocation")
assert(onceConnection.Connected == false)
signalOwner:Destroy()
print("GUIDEBOOK_CONNECTIONS_ENGINE_PASS hits=5 once=1")

افحص الفصل الصريح #

بعد الإطلاق الأول ينفّذ الاختبار Disconnect على الكائنات الثلاثة المحفوظة، ويفحص Connected==false لكل اتصال. لا ينبغي أن يغير Fire جديد يتبعه انتظار قيمة hits؛تبقى ثلاثة. وهكذا نتحقق من أن حدثاً لاحقاً لا يصل إلى المستمعين المفصولين. مجرد تعيين nil للمتغير مع نسيان فصل الكائن عملية أخرى، حتى لو اختفى المرجع من الكود الذي تنظر إليه.

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

اجعل إعادة الفتح عملية مُدارة #

تستدعي openPanel الأصلية closePanel قبل إنشاء اشتراك جديد وحفظه في currentConnection. لذلك تترك ثلاثة استدعاءات متتالية لـopenPanel مستمعاً نشطاً واحداً. يزيد Fire التالي hits من ثلاثة إلى أربعة. هذه إضافة callback واحدة، وليست ادعاء أن التجربة بكاملها نفذت callback واحدة منذ بدايتها. راقب فرق العداد إلى جانب قيمته الإجمالية حتى لا تخلط المرحلتين.

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

اجعل التنظيف المتكرر آمناً #

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

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

استجابات مؤكدةافتح الصورة بالحجم الكامل ↗
مخطط أصلي لنتائج اختبار Studio المعزول.

استخدم Once لأول استدعاء #

يقترح الدليل الرسمي Once عندما تكون الدالة مطلوبة لأول إطلاق للحدث فقط. ينشئ جزء منفصل onceConnection وعدّاداً جديداً onceHits. يُطلق الحدث مرتين؛بعد الانتظار يجب أن يصبح onceHits واحداً وConnected مساوية لـfalse. حُصل على هذا فعلياً في Roblox Studio باستخدام BindableEvent معزولة، وليس باستبدال محرك الأحداث بنموذج متزامن ثم افتراض أن النتيجة نفسها ستحدث.

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

احترم ترتيب معالجة الأحداث #

يفحص الاختبار النتيجة بعد task.wait التالية لـFire، ولا يفترض اكتمال callback في السطر التالي مباشرة. تشرح الوثائق الرسمية للأحداث المؤجلة الطوابير ونقاط الاستئناف بصورة مستقلة. زيادة متغير فوراً في نموذج محلي مصنوع يدوياً لا تثبت السلوك نفسه في المحرك. لذلك تدعم النتائج المؤكدة هنا تجربة صغيرة حقيقية لواجهة Roblox البرمجية، مع انتظار قبل قراءة العدادات.

ولا تفترض تطابق أثر Destroy وDisconnect في كل حالة callbacks معلّقة. يميّز القسم الرسمي للأحداث المؤجلة الفصل الصريح عن التدمير عندما توجد استدعاءات أُدرجت في الطابور. تفحص مجموعتنا الإطلاقات الجديدة بعد التنظيف وحالة Once، ولا تغطي كل الطوابير أو الدوال قيد التنفيذ أو المعالجة المتوازية. تحتاج تلك الحالات إلى أمثلة وفحوص منفصلة قبل تقديم ادعاءات أوسع عن النظام.

اختبر اللوحة الحقيقية كمرحلة تكامل #

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

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

المرحلةالمتوقع
ثلاثة مستمعينhits = 3
بعد Disconnectتبقى hits = 3
إعادة الفتحhits = 4
تنظيف مرتينتبقى hits = 4

احتفظ بالنتيجة المؤكدة قابلة للتكرار #

انتهت التجربة المصححة بالسطر GUIDEBOOK_CONNECTIONS_ENGINE_PASS hits=5 once=1 في Studio Output. يأتي السطر بعد assertions عن المستمعين الثلاثة وفصلهم وإعادة الفتح المُدارة والتنظيف الآمن وOnce. يحفظ المصدر التعليمي العمليات الدقيقة، ويحتفظ سجل التحقق ببصمته. يخص مجموع خمسة مراحل متتابعة من التجربة الكاملة، ولا يمثل عدد الاستجابات المتوقع لفعل واحد من اللاعب.

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

الحدالتغطية
لوحة جديدةhits = 5
Oncecallback واحدة
الواجهةGUI منفصلة
الطوابيرليست تغطية شاملة

المصادر الأصلية

Roblox Creator Hub — Events
Roblox — RBXScriptConnection
Roblox — RBXScriptSignal
Roblox — Deferred engine events