Roblox GuidebookBase de conocimiento
Español ⌄

Desarrollo / ROBLOX

Límites de solicitudes en Roblox: protege una pista del servidor de clics repetidos

Un limitador educativo para la pista del taller permite cuatro intentos inmediatos y recupera dos fichas por segundo. Explicamos estados separados de jugadores, tiempo del servidor, comportamiento del botón y pruebas locales sin tráfico real hacia Roblox.

Actualizado:

Elige una acción concreta para limitar #

Imagina un taller donde el principiante pulsa «¿Dónde está la estación?» para pedir una pista. Varios clics rápidos pueden ser accidentales, una reacción a una respuesta tardía o una repetición indeseada. Si cada solicitud inicia trabajo costoso, un botón pequeño puede generar carga innecesaria. Describe primero la operación del servidor y por qué necesita un límite de frecuencia.

Usamos una pista de texto, no una compra, una recompensa ni un guardado de perfil. Es nuestro escenario educativo original, no una función añadida a los juegos del autor del sitio. Cuatro intentos y dos fichas por segundo son parámetros elegidos para explicar. No son cuotas de Roblox ni recomendaciones universales. No se realizó una prueba de carga multijugador real.

El botón del cliente no establece la regla del servidor #

Desactivar temporalmente un botón y mostrar que espera una respuesta ayuda al jugador a entender que envió la solicitud. El servidor debe decidir de forma independiente si permite otra llamada. La documentación de Roblox advierte expresamente que no se debe confiar solo en un límite del cliente.

No aceptes como permiso el número de intentos restantes que declara el cliente ni su supuesto momento del clic anterior. Nuestro módulo guarda saldo y tiempo en el servidor. El cliente solicita la pista y el servidor decide. Controlar frecuencia no vuelve válidos los parámetros incorrectos: tipo, acción permitida y estado del juego siguen necesitando comprobaciones propias.

Una cubeta de fichas permite una serie breve #

Imagina una cubeta por jugador con capacidad máxima de cuatro fichas. Cada intento consume una. Se recuperan dos por segundo, pero nunca por encima de la capacidad. Así se permiten cuatro intentos inmediatos y una serie sostenida debe esperar la recuperación. Es el enfoque token bucket descrito por Roblox.

No significa «cuatro llamadas en cada segundo del calendario». Una espera breve puede recuperar solo una fracción de ficha. Hasta disponer de una entera, el siguiente intento se rechaza. Comprobamos esa recuperación fraccionaria para poder explicar la demora, en lugar de presentarla como un fallo aleatorio del botón.

Cubeta educativa: 4 y 2/sAbrir imagen a tamaño completo ↗
Ruta ficticia de prueba local. El tiempo se mide desde el inicio del ejercicio.
TiempoIntentoResultado y saldo
0Primero a cuartoPermitidos: 4 → 0
0QuintoRechazado: 0
0.25 sNuevo intentoRechazado: 0,5
0.5 sNuevo intentoPermitido: 0

Separa el estado de cada jugador #

Si A consume sus cuatro fichas, la siguiente petición de B no debería bloquearse por A. Las cubetas usan como clave el Player que recibe OnServerEvent en el servidor. No sustituyas ese objeto por un nombre o identificador enviado como argumento adicional del cliente. De lo contrario, elegir una cubeta ajena dependería de entrada no fiable.

Las cubetas independientes no limitan el tráfico total de todos los jugadores. Muchos usuarios pueden consumir sus intentos permitidos al mismo tiempo. Las operaciones caras o las llamadas al backend requieren analizar por separado frecuencia total, colas y coste. El módulo no incorpora presupuesto compartido del servidor, límite de tareas concurrentes ni estado distribuido entre servidores.

Comprende el ModuleScript educativo #

Coloca HintRequestLimiter en ServerScriptService. new(capacity, refillPerSecond, clock) configura capacidad, recuperación y función del reloj del servidor. Allow(player) devuelve permiso y un código breve. Forget(player) elimina el estado local al salir. El módulo no accede a DataStore, no crea RemoteEvent ni envía la pista por sí mismo.

La capacidad debe ser un entero entre 1 y 1000. La recuperación debe ser positiva, finita y no superar 1000. Son límites elegidos para este ejemplo, no cuotas de la plataforma. Se permiten velocidades fraccionarias. Un reloj inválido o fallido provoca rechazo; retroceder el tiempo no añade fichas. Todo el estado pertenece únicamente al servidor actual.

os.clock no tiene una referencia absoluta especificada. Compara diferencias, no fechas del calendario. El módulo acepta una referencia negativa finita; una prueba local separada verifica la recuperación con ese reloj.

-- 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

Conecta deliberadamente el manejador #

Crea un RemoteEvent GetHint en ReplicatedStorage y un Script del servidor junto al módulo. El Script obtiene los servicios, carga el módulo con require y construye el limitador con 4, 2 y os.clock. El manejador utiliza el Player proporcionado por Roblox, comprueba la solicitud y envía solo el texto predefinido a ese jugador.

El ejemplo espera el atributo TutorialStep=1 mantenido por la lógica existente del tutorial en el servidor. No inicia el tutorial ni modifica el atributo a petición del cliente. Sin ese estado, no envía la pista. Tampoco incluye interfaz del cliente: conéctala por separado con GetHint y su respuesta. El Script demuestra integración, no un juego completo probado.

Ruta de la pista del servidorAbrir imagen a tamaño completo ↗
Diagrama original del manejador: una ficha no sustituye validación de entrada y estado.
-- 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)

Define qué consume un intento #

Nuestro manejador llama a Allow antes de validar la cadena. Por eso un intento consume una ficha incluso con nombre de pista incorrecto o etapa inadecuada. Es intencional: se limita la entrada al manejador y no solo las pistas entregadas con éxito. La entrada inválida no obtiene un camino gratuito ilimitado hacia el procesamiento posterior.

No devuelvas automáticamente la ficha tras cada rechazo, porque una secuencia de solicitudes incorrectas podría saltarse esa regla. El significado del juego aún exige validación independiente. Una ficha no prueba derecho a una recompensa, propiedad de un objeto, tarea terminada ni permiso para cambiar objetos ajenos. Otra tarea puede requerir distinto orden o coste, definidos expresamente.

No contestes cada repetición rechazada #

El manejador educativo termina silenciosamente una solicitud limitada. Enviar una respuesta o escribir una línea detallada por cada rechazo podría generar otro flujo de trabajo en esa ruta. No conviertas el limitador en una fuente interminable de notificaciones. Para observarlo, elige una forma acotada de resumir rechazos, en lugar de guardar cada paquete recibido.

El rechazo silencioso exige que la interfaz reactive el botón tras una espera local razonable y explique que se puede intentar otra vez. Aquí no implementamos ese temporizador del cliente. No dejes el botón desactivado para siempre esperando una respuesta que el servidor deliberadamente no manda. Comodidad de interfaz y decisión de permiso son tareas distintas.

Prueba con un reloj controlado #

La prueba local de Luau inyecta su propia función de reloj. En el momento 0, A realiza cuatro intentos permitidos; el quinto devuelve RateLimited. En 0,25 solo se recuperó media ficha y el intento se rechaza. En 0,5 hay una ficha: un intento se permite y el siguiente no. Estas cifras describen una prueba local, no visitas reales.

La prueba también verifica la cubeta independiente de B. Una pausa larga devuelve como máximo cuatro intentos. Un reloj inválido o que retrocede no crea saldo adicional. Se comprueban configuración, limpieza y velocidades fraccionarias. Todo usa objetos sustitutos y tiempo controlado, sin remotes reales de Roblox, jugadores ni mediciones de rendimiento del servidor.

PruebaResultado esperado
Quinto intento inmediatoRateLimited
Solicitud de otro jugadorCubeta independiente
Pausa prolongadaComo máximo 4 fichas
El reloj retrocedeSin recuperación
Reloj inválidoInvalidClock
Estado después de limpiarNueva cubeta llena

Limpia el estado al salir #

Conecta Players.PlayerRemoving con limiter:Forget(player). De lo contrario, la tabla conserva el estado de jugadores que salieron hasta finalizar el servidor. No llames Forget tras un rechazo normal: la siguiente petición recibiría una cubeta llena, anulando el límite. La limpieza pertenece al ciclo de vida del jugador y no sirve para recuperar el permiso de un botón.

Una nueva entrada después de limpiar comienza con una cubeta nueva. Es el comportamiento local previsto, no una defensa contra reconexiones ni un límite global de cuenta. El estado no persiste entre servidores. Los requisitos que abarcan varias sesiones necesitan otro diseño; borrar una entrada de tabla no demuestra esas garantías.

Limitar frecuencia no limita todo el trabajo posterior #

Una solicitud permitida puede iniciar una tarea prolongada en otro lugar. Nuestro módulo no espera su finalización ni cuenta las tareas en marcha. Incluso la frecuencia permitida puede ser excesiva para una operación costosa. Revisa qué sucede después: ¿se clonan modelos grandes, se llaman APIs o se afecta a otros jugadores?

Compras, guardado y economía compartida requieren sus propias reglas, no solo fichas. El ejemplo tampoco comprueba distancia a una estación física: la pista es deliberadamente textual y no exige esa condición. Al adaptarlo a objetos, añade las validaciones necesarias de posición, permisos y estado del servidor. No cambiamos código de los juegos existentes del autor.

Prepara la entrega de desarrollo #

Guarda juntos la acción limitada, el significado de gastar un intento, capacidad, recuperación y comprobaciones necesarias del servidor. Añade las rutas de prueba local e indica que red y carga reales siguen pendientes. No transmitas cuatro como ajuste universal para cualquier botón o arma; pertenece únicamente al ejercicio.

Pide al desarrollador comprobar dos jugadores, pausa larga, salida y solicitud inválida, además de recuperar el botón después de un rechazo silencioso. Enumera aparte lo ausente: presupuesto compartido, operaciones concurrentes, estado entre sesiones e interfaz del cliente. Así la idea sirve para el próximo juego sin ocultar su grado real de preparación.

Fuentes originales

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