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

التطوير / ROBLOX

حدود الطلبات في Roblox: حماية تلميح الخادم من النقر المتكرر

نبني محدداً تعليمياً لتلميح الورشة: أربع محاولات فورية ثم تجدد وحدتان كل ثانية. نشرح حالة مستقلة للاعبين ووقت الخادم واستعادة الزر واختبارات محلية دون إرسال حركة اختبار حقيقية إلى Roblox.

آخر تحديث:

اختر فعلاً محدداً لتقييده #

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

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

زر العميل لا يحدد قاعدة الخادم #

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

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

دلو الوحدات يسمح بسلسلة قصيرة #

تخيل لكل لاعب دلواً يتسع لأربع وحدات. تستهلك المحاولة وحدة واحدة. تعود وحدتان كل ثانية، لكن الرصيد لا يتجاوز السعة. لذلك يمكن إجراء أربع محاولات فوراً، بينما تحتاج السلسلة المستمرة إلى انتظار التجدد. هذا نهج token bucket الذي تصفه Roblox.

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

دلو تعليمي: 4 وتجدد 2/ثافتح الصورة بالحجم الكامل ↗
مسار اختبار محلي خيالي؛ الوقت نسبي إلى بداية التمرين.
الوقتالمحاولةالنتيجة والرصيد
0الأولى إلى الرابعةمسموح: 4 → 0
0الخامسةمرفوض: 0
0.25 sمحاولة جديدةمرفوض: 0.5
0.5 sمحاولة جديدةمسموح: 0

افصل حالة كل لاعب #

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

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

افهم ModuleScript التعليمي #

ضع HintRequestLimiter في ServerScriptService. يحدد new(capacity, refillPerSecond, clock) السعة ومعدل التجدد ودالة ساعة الخادم. يعيد Allow(player) الإذن ورمز نتيجة قصيراً. يحذف Forget(player) الحالة المحلية عند المغادرة. لا يصل النموذج إلى DataStore ولا ينشئ RemoteEvent ولا يرسل التلميح بنفسه.

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

لا تملك os.clock نقطة بداية مطلقة محددة. قارن فروق الوقت لا تواريخ التقويم. يقبل المثال بداية سالبة محدودة؛ يفحص اختبار محلي منفصل التجدد بهذه الساعة.

-- Original teaching limiter for server-side hint requests.
-- Pass the Player supplied by OnServerEvent, never a client-supplied identity.
-- Frequency control does not validate permissions or grant rewards.
local Limiter = {}

local function finite(value)
    return type(value) == "number" and value == value
        and value > -math.huge and value < math.huge
end

function Limiter.new(capacity, refillPerSecond, clock)
    assert(finite(capacity) and capacity == math.floor(capacity)
        and capacity >= 1 and capacity <= 1000, "Invalid capacity")
    assert(finite(refillPerSecond) and refillPerSecond > 0
        and refillPerSecond <= 1000, "Invalid refill rate")
    if clock == nil then clock = os.clock end
    assert(type(clock) == "function", "Invalid clock")
    local buckets = {}
    local adapter = {}

    function adapter:Allow(player)
        if player == nil then return false, "MissingPlayer" end
        local ok, now = pcall(clock)
        if not ok or not finite(now) then
            return false, "InvalidClock"
        end
        local bucket = buckets[player]
        if not bucket then
            bucket = {tokens = capacity, last = now}
            buckets[player] = bucket
        elseif now < bucket.last then
            return false, "ClockWentBackwards"
        else
            bucket.tokens = math.min(capacity,
                bucket.tokens + (now - bucket.last) * refillPerSecond)
            bucket.last = now
        end
        if bucket.tokens < 1 then return false, "RateLimited" end
        bucket.tokens -= 1
        return true, "Allowed"
    end

    function adapter:Forget(player)
        buckets[player] = nil
    end
    return adapter
end

return Limiter

اربط معالج الخادم بوضوح #

أنشئ RemoteEvent باسم GetHint في ReplicatedStorage وScript خادم بجوار الوحدة. يحصل Script على الخدمات ويحمل الوحدة باستخدام require وينشئ المحدد بقيم 4 و2 وos.clock. يستخدم المعالج Player الذي توفره Roblox، ويفحص الطلب ثم يرسل النص المحدد مسبقاً إلى ذلك اللاعب فقط.

يتوقع المثال الخاصية TutorialStep=1 التي يديرها منطق التعليم الموجود في الخادم. لا يبدأ التعليم ولا يغير الخاصية بطلب من العميل. إذا غابت هذه الحالة فلن يرسل تلميحاً. واجهة العميل غير موجودة أيضاً؛ يجب ربطها بصورة منفصلة مع GetHint والرد. يوضح Script التكامل، وليس لعبة كاملة اختبرت فعلياً.

مسار طلب تلميح الخادمافتح الصورة بالحجم الكامل ↗
رسم أصلي للمعالج؛ الوحدة لا تعوض فحص الإدخال وحالة اللعب.
-- Server Script; create ReplicatedStorage.GetHint as a RemoteEvent first.
-- TutorialStep must be maintained by your existing SERVER gameplay logic.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Limiter = require(
    game:GetService("ServerScriptService"):WaitForChild("HintRequestLimiter")
)
local request = ReplicatedStorage:WaitForChild("GetHint")
assert(request:IsA("RemoteEvent"), "GetHint must be a RemoteEvent")
local limiter = Limiter.new(4, 2, os.clock)

request.OnServerEvent:Connect(function(player, hintName)
    -- Each attempt consumes quota before further processing.
    if not limiter:Allow(player) then return end
    if type(hintName) ~= "string" or #hintName > 32 then return end
    if hintName ~= "FindWorkshop" then return end
    if player:GetAttribute("TutorialStep") ~= 1 then return end
    local character = player.Character
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    if not humanoid or humanoid.Health <= 0 then return end
    request:FireClient(player, "Hint", "Look for the workshop sign.")
end)

Players.PlayerRemoving:Connect(function(player)
    limiter:Forget(player)
end)

حدد ما يستهلك محاولة #

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

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

لا تجب عن كل تكرار مرفوض #

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

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

اختبر بساعة مضبوطة #

يمرر اختبار Luau المحلي دالة ساعة خاصة. عند الزمن 0 يجري A أربع محاولات مسموحة؛ تعيد الخامسة RateLimited. عند 0.25 تجدد نصف وحدة فقط فيرفض الطلب. عند 0.5 تتوفر وحدة: تنجح محاولة ولا تنجح التالية. هذه أرقام اختبار محلي وليست زيارات فعلية للعبة.

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

الفحصالمتوقع
محاولة خامسة فوراًRateLimited
طلب لاعب آخردلو مستقل
خمول طويلأربع وحدات على الأكثر
رجوع الساعةلا تجدد
ساعة غير صالحةInvalidClock
الحالة بعد التنظيفدلو جديد ممتلئ

نظف الحالة عند المغادرة #

اربط Players.PlayerRemoving مع limiter:Forget(player). وإلا يحتفظ الجدول بحالة اللاعبين المغادرين حتى ينتهي الخادم. لا تستدع Forget بعد رفض عادي: سيحصل الطلب التالي على دلو جديد ممتلئ وتزول فائدة الحد. التنظيف جزء من دورة حياة اللاعب، وليس أداة لاستعادة إذن الزر.

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

تقييد التكرار لا يحد كل العمل اللاحق #

قد يبدأ الطلب المسموح مهمة طويلة في دالة أخرى. لا ينتظر نموذجنا انتهاءها ولا يعد المهام التي بدأت. قد يكون المعدل المسموح نفسه كبيراً لعملية مكلفة. افحص ما يحدث بعد الإذن: هل تنسخ نماذج كبيرة أو تتصل بـ API أو تؤثر في لاعبين آخرين؟

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

جهز تسليم التطوير #

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

اطلب من المطور فحص لاعبين وخمول طويل ومغادرة وطلب غير صالح، واستعادة الزر بعد رفض صامت. اذكر الوظائف الغائبة منفصلة: ميزانية خادم مشتركة وعمليات متزامنة وحالة بين الجلسات وواجهة عميل. هكذا تستخدم الفكرة في اللعبة التالية مع بقاء حدود الجاهزية واضحة.

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

Roblox Creator Hub — Client-server boundary
Luau — Standard library
Roblox Creator Hub — os