Una cuenta atrás parece lo más fácil del mundo: un bucle, un wait(1), y a restar. Y funciona… hasta que la pones a contar cinco minutos y descubres que ha tardado cinco minutos y veinte segundos.
No está roto tu código: es que esperar «un segundo» no es esperar un segundo. Y para una ronda de juego, ese error se acumula.
En la demo tienes tres relojes contando a la vez: el ingenuo, el bueno y el que va contra la hora real. Déjalos correr y mira cómo se separan.
Respuesta rápida
No cuentes restando: guarda cuándo termina y calcula lo que falta. Y usa task.wait(), no el viejo wait(). El temporizador que manda vive en el servidor; el cliente solo pinta el número.
Lo que necesitas antes de empezar
- Un lugar en Studio y ganas de mirar la consola.
- Unos 20 minutos.
Paso 1: por qué se desvía el bucle ingenuo
-- El que escribe todo el mundo
local restante = 300
while restante > 0 do
task.wait(1)
restante = restante - 1
actualizar(restante)
end
El problema: task.wait(1) no espera un segundo, espera al menos un segundo. Espera al siguiente ciclo del motor después de ese segundo, así que siempre se pasa un poco. Ese poco, trescientas veces, son varios segundos de más.
Y hay un segundo problema, peor: si el servidor tiene un pico de trabajo, ese wait puede tardar bastante más, y tu cuenta atrás se retrasa sin que nadie se entere.
Paso 2: el temporizador que no se desvía
La idea: no cuentes cuántas veces has esperado. Apunta el momento de terminar y resta.
local DURACION = 300
local terminaEn = os.clock() + DURACION
while true do
local restante = terminaEn - os.clock()
if restante <= 0 then break end
actualizar(math.ceil(restante))
task.wait(0.2) -- la frecuencia de refresco, no la cuenta
end
alTerminar()
Ahora task.wait(0.2) solo decide cada cuánto se refresca el número en pantalla. Aunque se retrase, el tiempo restante se calcula desde el reloj y siempre sale bien. Si el servidor se atasca dos segundos, al volver el número da un salto: correcto, porque el tiempo ha pasado igual.
Esa es toda la diferencia: el reloj manda, el bucle solo mira.

Paso 3: wait, task.wait y los demás
| Función | Qué hace | Cuándo usarla |
|---|---|---|
task.wait(n) | Espera al menos n segundos y devuelve lo que esperó de verdad | Siempre que esperes |
task.wait() | Espera un ciclo (un fotograma, más o menos) | Repartir trabajo entre fotogramas |
task.delay(n, f) | Ejecuta f dentro de n segundos, sin bloquear | Cosas «para luego» |
task.spawn(f) | Lanza f en paralelo ahora mismo | Hacer dos cosas a la vez |
wait(n) | Lo viejo: menos preciso y desaconsejado | Nunca en código nuevo |
task.wait devuelve el tiempo que ha esperado de verdad, que es muy útil:
local esperado = task.wait(1)
print(esperado) -- 1.0166... o lo que toque, nunca exactamente 1
Ese decimal de más es exactamente el desvío del que va este artículo.
Paso 4: el reloj compartido de una ronda
Para una ronda que todos ven igual, el servidor manda el momento de terminar, no los segundos que quedan:
-- servidor
local valor = Instance.new("NumberValue")
valor.Name = "FinDeRonda"
valor.Parent = game:GetService("ReplicatedStorage")
local function empezarRonda(duracion)
valor.Value = workspace:GetServerTimeNow() + duracion
end
-- cliente
local valor = game:GetService("ReplicatedStorage"):WaitForChild("FinDeRonda")
local etiqueta = script.Parent
game:GetService("RunService").Heartbeat:Connect(function()
local restante = valor.Value - workspace:GetServerTimeNow()
if restante < 0 then restante = 0 end
local m = math.floor(restante / 60)
local s = math.floor(restante % 60)
etiqueta.Text = string.format("%d:%02d", m, s)
end)
workspace:GetServerTimeNow() es la hora del servidor vista desde el cliente, ya ajustada por la latencia. Así, el número que ve cada jugador es el mismo, sin mandar un mensaje por segundo a nadie.
Mandar los segundos restantes por un RemoteEvent cada segundo también funciona, pero gasta red y se ve a saltos.
Paso 5: formatear el tiempo
local function formatear(segundos)
segundos = math.max(0, math.floor(segundos))
local h = math.floor(segundos / 3600)
local m = math.floor((segundos % 3600) / 60)
local s = segundos % 60
if h > 0 then
return string.format("%d:%02d:%02d", h, m, s)
end
return string.format("%d:%02d", m, s)
end
El %02d es lo que pone el cero delante: sin él tienes «3:7» en vez de «3:07», que es de las cosas que más cutre hacen ver una interfaz.
Paso 6: parar, pausar y cancelar
Un temporizador que no se puede cancelar es un problema en cuanto tengas dos rondas seguidas:
local rondaActual = 0
local function empezar(duracion)
rondaActual = rondaActual + 1
local mia = rondaActual
local terminaEn = os.clock() + duracion
task.spawn(function()
while rondaActual == mia do -- si empieza otra ronda, esta muere sola
local restante = terminaEn - os.clock()
if restante <= 0 then
if rondaActual == mia then alTerminar() end
return
end
task.wait(0.2)
end
end)
end
Ese truco del identificador de ronda es la forma más simple de cancelar un bucle: el bucle comprueba si sigue siendo el actual. Sin eso, empezar una ronda nueva deja la anterior corriendo por debajo, y acabas con dos cuentas atrás peleándose por la misma etiqueta.
Adivina la salida
Cuánto se desvía la cuenta atrás ingenua
Elige cuánto dura la ronda y simula los dos temporizadores. Cada espera se pasa un poco, como en el motor de verdad; aquí se ve acumulado.
Los dos bucles
while restante > 0 do task.wait(1) restante = restante - 1 end
local fin = os.clock() + DURACION while true do local r = fin - os.clock() if r <= 0 then break end task.wait(0.2) end
Errores habituales
La cuenta atrás llega tarde
Estás restando de uno en uno. Guarda el momento de terminar y resta del reloj.
Cada jugador ve un número distinto
Cada cliente lleva su propia cuenta. Comparte el momento de fin y que cada uno calcule.
Al empezar otra ronda hay dos cuentas a la vez
El bucle viejo sigue vivo. Necesitas la comprobación de «¿sigo siendo la ronda actual?».
El número parpadea entre 3 y 4
Estás mezclando floor y ceil en sitios distintos, o refrescando muy rápido con redondeos distintos. Elige uno: math.ceil para la cuenta atrás va bien (enseña 1 hasta que de verdad llega a cero).
Usa wait() y va peor cuando hay gente
wait() es el viejo. task.wait() en todo el código nuevo.
El temporizador lo lleva el cliente
Entonces no es un temporizador: es una sugerencia. Cualquiera puede acelerarlo. Lo que decide cuándo acaba la ronda es el servidor.
Preguntas frecuentes
¿os.clock, os.time o tick?
os.clock() para medir diferencias (es monótono y tiene decimales). os.time() para fechas y cosas que tienen que sobrevivir entre sesiones, como las recompensas diarias. tick() está desaconsejado.
¿Cómo hago una barra de tiempo en vez de un número?
Con la misma cuenta: barra.Size = UDim2.fromScale(restante / duracion, 1). Y anímala con TweenService si quieres que vaya suave: mover cosas con TweenService.
¿Puedo tener un temporizador que siga corriendo si el jugador se va?
En el servidor, sí: mientras el servidor viva, el temporizador sigue. Lo que no sobrevive es al cierre del servidor; para eso hay que guardar la fecha de fin en el DataStore.
¿Cada cuánto debo refrescar la pantalla?
Con cinco veces por segundo (task.wait(0.2)) va sobrado para segundos enteros. Refrescar en cada fotograma solo tiene sentido si enseñas décimas.
Última revisión: 2 de septiembre de 2026





Deja una respuesta