Cómo hacer un temporizador y una cuenta atrás

Claim Your Loot

·

Portada del artículo «Un temporizador que no se desvía», de la sección Scripting en Luau

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.

El desvío acumulado de contar esperas frente a calcular contra el reloj

Paso 3: wait, task.wait y los demás

FunciónQué haceCuándo usarla
task.wait(n)Espera al menos n segundos y devuelve lo que esperó de verdadSiempre 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 bloquearCosas «para luego»
task.spawn(f)Lanza f en paralelo ahora mismoHacer dos cosas a la vez
wait(n)Lo viejo: menos preciso y desaconsejadoNunca 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.

Restando de uno en uno
tiempo real que ha tardado
Contra el reloj
tiempo real que ha tardado

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
El de la izquierda cuenta esperas; el de la derecha mira el reloj

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

Claim Your Loot

Explora otras secciones

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *