Desarrollo / ROBLOX
Conexiones de eventos Roblox: no añadas otro listener cada vez que abres un menú
Sigue una suscripción propia desde su creación hasta la reapertura, desconexión y nueva apertura. Un experimento original en Studio compara tres listeners activos con una conexión administrada y comprueba Once por separado.
Elige un mecanismo de fallo para investigar #
Imagina un panel temporal de ayuda que responde a un evento. Su controlador conecta una función nueva cada vez que se abre, pero deja activa la suscripción anterior. Tras varias aperturas, un evento puede ejecutar varias funciones. Es un mecanismo posible, no un diagnóstico de cualquier botón existente. Primero identifica qué función crea la conexión y qué componente debe terminar su trabajo.
El ejercicio no crea un menú real ni pulsa su botón. Las funciones openPanel y closePanel representan el ciclo de un controlador imaginario; un BindableEvent independiente proporciona la señal. Así comprobamos la responsabilidad sobre las suscripciones sin diseñar la interfaz ni modificar juegos publicados. Probar un panel verdadero sigue siendo una etapa de integración separada, aunque estas assertions ya hayan pasado.
Separa el evento de su conexión #
La documentación oficial describe Connect como una forma de asociar una función a un evento. Devuelve un objeto RBXScriptConnection. Conservarlo permite al propietario desconectar esa suscripción concreta mediante Disconnect. La señal, la función callback y la conexión devuelta son elementos diferentes. Una variable que contiene la función no se convierte automáticamente en una referencia al objeto de conexión.
En nuestro ejemplo, el controlador posee currentConnection. Sabe dónde se crea la suscripción y utiliza la misma referencia al cerrar. No repartas su creación entre funciones independientes sin un propietario claro. Para este acuerdo basta un campo: contiene la conexión actual o queda en nil después de limpiarla. Esa convención pequeña permite seguir toda la responsabilidad desde el comienzo hasta el final.
Reproduce tres listeners no deseados #
La primera parte del test original crea tres conexiones a un BindableEvent. Cada callback incrementa el contador compartido hits. Después se dispara el evento una vez. Tras esperar el procesamiento, hits debe valer tres. Esto comprueba que tres suscripciones activas producen tres callbacks en ese escenario aislado. No pretende reproducir el botón defectuoso de una experiencia existente concreta.
Guarda cada conexión devuelta en la tabla duplicates para desconectar después todos los listeners creados por la comparación. El caso inicial hace reproducible la diferencia respecto al diseño administrado. No llames número de pulsaciones a la cantidad de suscripciones. Hay una emisión del evento y tres funciones conectadas por separado; son dos cantidades diferentes que explican aspectos distintos del mismo resultado.
-- 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")Comprueba la desconexión explícita #
Después de la primera emisión, el test llama a Disconnect en los tres objetos guardados. Comprueba Connected==false en cada uno. Otro Fire seguido de una espera no debe cambiar hits: permanece en tres. Verificamos así que un evento posterior no alcanza esos listeners desconectados. Poner únicamente una variable en nil y olvidar desconectar el objeto sería una operación diferente.
Primero desconecta la suscripción que pertenece al controlador y después libera su referencia guardada. Evita desconectar conexiones ajenas creadas por otro sistema. En una interfaz real, anotar el propietario junto al punto de creación ayuda a entender la limpieza. El objetivo es terminar el trabajo de un componente concreto, no impedir globalmente que todos los listeners del proyecto reciban señales.
Administra la reapertura #
Nuestra openPanel llama a closePanel antes de crear una suscripción nueva y guardarla en currentConnection. Por eso, tres llamadas consecutivas dejan un listener activo. El siguiente Fire incrementa hits de tres a cuatro. Es una comprobación de un callback adicional, no una afirmación de que todo el experimento solo haya ejecutado un callback desde su inicio.
Ese comportamiento encaja con nuestro acuerdo de un controlador actual del panel. No es una arquitectura universal: varias ventanas independientes pueden necesitar propietarios y referencias distintos. Define la cantidad de suscripciones activas que deseas antes de elegir la función auxiliar. Después compruébala mediante un resultado observable. Un código corto o un nombre familiar no demuestra que exista el número correcto de listeners.
Haz segura la limpieza repetida #
La función closePanel comprueba si existe currentConnection. Si existe, desconecta la suscripción y establece el campo en nil. Una segunda llamada no intenta utilizar una referencia liberada. El test llama a closePanel dos veces y confirma que un evento posterior no cambia hits. Esto comprueba la finalización repetida de nuestro controlador, no todos los posibles fallos de ciclo de vida de una interfaz completa.
Después se ejecuta openPanel otra vez. Un Fire debe incrementar hits hasta cinco antes de la limpieza final. Ese paso importa: detener los callbacks no basta si después nunca puedes volver a crear el controlador. Comprueba ambas direcciones por separado: la limpieza termina la respuesta antigua y un controlador recién abierto vuelve a responder una vez según el acuerdo elegido.
Usa Once para la primera ejecución #
La guía oficial ofrece Once cuando necesitas el callback únicamente para la primera emisión del evento. Una sección independiente crea onceConnection y un contador nuevo onceHits. El evento se dispara dos veces. Tras esperar, onceHits debe valer uno y Connected debe ser false. El resultado se obtuvo realmente en Roblox Studio con un BindableEvent aislado, no con un modelo síncrono sustitutivo.
La primera señal no necesariamente es la primera situación adecuada para tu lógica. Si el callback comprueba una condición adicional, decide antes si debe seguir escuchando después de una entrada inapropiada. No escondas ese acuerdo detrás del nombre Once. Una acción correcta de una sola ejecución y la primera señal independientemente de sus argumentos pueden exigir diseños y expectativas diferentes.
Respeta el orden de procesamiento #
El test examina los resultados después de task.wait tras Fire. No supone que el callback haya terminado en la línea siguiente. La documentación oficial de eventos diferidos explica las colas y los puntos de reanudación por separado. Incrementar una variable de forma síncrona en un modelo casero no demostraría el mismo comportamiento del motor. Por eso usamos un experimento real con la API de Roblox para los resultados confirmados.
Tampoco supongas que Destroy y Disconnect tienen efectos idénticos en todas las situaciones con callbacks pendientes. El apartado oficial de eventos diferidos distingue desconexión explícita y destrucción cuando ya hay llamadas en cola. Nuestra suite comprueba nuevas emisiones después de la limpieza y el caso Once. No cubre todas las colas, funciones ya ejecutándose o procesamiento paralelo. Esas situaciones necesitan ejemplos y pruebas independientes.
Prueba el panel real como integración #
Para integrar después, escribe una secuencia: crear el controlador, abrirlo repetidamente, emitir el evento, cerrar dos veces, emitir después del cierre y reabrir. Considera la destrucción del objeto UI y el trabajo ya iniciado como casos adicionales. Si cambia el número de respuestas, compara las conexiones creadas con el ciclo previsto antes de añadir una espera o debounce. Una pausa podría ocultar el síntoma sin corregir la responsabilidad.
Un límite de frecuencia responde cuántas acciones se permiten durante un tiempo. La responsabilidad de conexiones responde cuántos listeners existen y cuándo termina su trabajo. Ninguno sustituye al otro. Este ejercicio no contiene solicitudes de red, recompensas, datos guardados ni compras. Al añadirlos, crea sus propias comprobaciones de resultado en lugar de extender la evidencia local a una afirmación sobre todo el juego.
| Etapa | Esperado |
|---|---|
| Tres listeners | hits = 3 |
| Después de Disconnect | hits sigue en 3 |
| Reapertura repetida | hits = 4 |
| Doble limpieza | hits sigue en 4 |
Conserva el resultado confirmado #
El experimento corregido terminó con GUIDEBOOK_CONNECTIONS_ENGINE_PASS hits=5 once=1 en Studio Output. La línea aparece después de assertions sobre tres listeners, desconexión, reapertura administrada, limpieza segura y Once. El archivo didáctico conserva las operaciones exactas y su registro guarda la suma de comprobación. El total cinco corresponde a etapas sucesivas del experimento completo, no a una acción del jugador.
Para entregar a otro desarrollador, conserva juntos el código, la línea esperada, la secuencia y los límites de verificación. No lo llames prueba de un panel acabado: el experimento no contiene un panel. El proyecto aislado no modifica juegos publicados y comprobar la interfaz verdadera sigue siendo la siguiente tarea. Una descripción precisa hace repetibles el mecanismo del fallo y el ciclo propuesto sin exagerar lo que se comprobó.
| Límite | Cobertura |
|---|---|
| Panel nuevo | hits = 5 |
| Once | Un callback |
| Interfaz | GUI por separado |
| Colas | No es exhaustivo |
Fuentes originales
Roblox Creator Hub — EventsRoblox — RBXScriptConnection
Roblox — RBXScriptSignal
Roblox — Deferred engine events