Таймер раунда должен считать время на сервере, а интерфейс каждого игрока только показывать остаток. Мы соберём цикл «перерыв → раунд → финиш», сохраним фазу и точное время окончания в атрибутах ReplicatedStorage и синхронизируем TextLabel без RemoteEvent на каждую секунду.
Если каждый LocalScript уменьшает своё число раз в секунду, таймеры начинают расходиться после лагов, сворачивания окна или позднего входа игрока. Надёжнее хранить не «осталось 37», а момент окончания раунда. Сервер задаёт одну временную точку, Roblox реплицирует атрибуты, а клиент вычисляет остаток через GetServerTimeNow. Новый игрок сразу читает текущее состояние.
RoundLoop переключает фазы и задаёт общее время окончания.
RoundPhase и RoundEndsAt автоматически видны клиентам.
LocalScript вычисляет секунды и обновляет один TextLabel.
Из каких частей состоит игровой раунд
Создайте ScreenGui с именем RoundGui в StarterGui, внутри TextLabel с именем TimerLabel и LocalScript. Серверный Script RoundLoop положите в ServerScriptService. Сервер будет менять два атрибута на ReplicatedStorage: строку RoundPhase и число RoundEndsAt. UI никогда не запускает раунд и не решает, кто победил.
| Элемент | Где находится | Ответственность |
|---|---|---|
| RoundLoop | ServerScriptService | Фазы, начало и окончание |
| RoundPhase | Атрибут ReplicatedStorage | Название текущей фазы |
| RoundEndsAt | Атрибут ReplicatedStorage | Общий момент окончания |
| TimerLabel | StarterGui | Текст для конкретного игрока |
Сервер является источником истины. Клиент получает только данные для отображения, поэтому изменение LocalScript у одного игрока не ускорит и не остановит сам раунд.

Сервер переключает фазы по кругу. После финиша начинается новый перерыв
Пишем серверный цикл и клиентский таймер
Первая часть работает на сервере. Она публикует фазу и дедлайн, затем ждёт до этого дедлайна. Вторая часть находится рядом с TimerLabel и рисует остаток каждый кадр. GetServerTimeNow даёт клиентам согласованную оценку серверного времени, а math.ceil не показывает ноль слишком рано.
-- RoundLoop.server.lua в ServerScriptService
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local INTERMISSION_SECONDS = 10
local ROUND_SECONDS = 60
local FINISH_SECONDS = 3
local function publishPhase(phase: string, duration: number)
local endsAt = workspace:GetServerTimeNow() + duration
ReplicatedStorage:SetAttribute("RoundPhase", phase)
ReplicatedStorage:SetAttribute("RoundEndsAt", endsAt)
repeat
task.wait(0.25)
until workspace:GetServerTimeNow() >= endsAt
end
local function setPlayersInRound(inRound: boolean)
for _, player in Players:GetPlayers() do
player:SetAttribute("InRound", inRound)
end
end
while true do
setPlayersInRound(false)
publishPhase("Перерыв", INTERMISSION_SECONDS)
setPlayersInRound(true)
publishPhase("Раунд", ROUND_SECONDS)
setPlayersInRound(false)
publishPhase("Финиш", FINISH_SECONDS)
end
-- RoundTimer.client.lua рядом с TimerLabel
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local label = script.Parent
local function formatTime(seconds: number): string
local minutes = math.floor(seconds / 60)
local rest = seconds % 60
return string.format("%02d:%02d", minutes, rest)
end
RunService.Heartbeat:Connect(function()
local phase = ReplicatedStorage:GetAttribute("RoundPhase") or "Подготовка"
local endsAt = ReplicatedStorage:GetAttribute("RoundEndsAt") or 0
local remaining = math.max(
0,
math.ceil(endsAt - workspace:GetServerTimeNow())
)
label.Text = string.format("%s %s", phase, formatTime(remaining))
end)Все игроки видят одну фазу и близкий остаток времени. Поздно вошедший игрок читает актуальные атрибуты без отдельного запроса. Лаг одного клиента не меняет момент завершения раунда. После финиша цикл снова переходит к перерыву.

Один подход накапливает задержку. Сравнение показывает, какой вариант легче поддерживать и проверять.
Почему нельзя просто вычитать по одной секунде
Цикл task.wait(1) не обещает возобновиться ровно через секунду: планировщик продолжит поток на подходящем шаге после указанной задержки. Если пятьдесят раз вычитать единицу после каждого ожидания, маленькие задержки накапливаются. Дедлайн решает проблему. На каждом обновлении мы считаем endsAt − GetServerTimeNow, поэтому потерянный кадр влияет только на плавность надписи, а не на длительность раунда.
| Подход | После лага | Поздний вход |
|---|---|---|
| Локальная переменная -= 1 | Таймер отстаёт | Начинает с неверного числа |
| RemoteEvent каждую секунду | Лучше, но много сообщений | Нужна отдельная синхронизация |
| Атрибуты + дедлайн | Сразу догоняет время | Читает текущие значения |
| Решение раунда на клиенте | Можно подменить | Нет общего состояния |
Атрибуты подходят для небольшого серверного состояния, которое должны видеть все клиенты. Roblox реплицирует их автоматически. При этом порядок двух разных типов обновлений нельзя предполагать наугад, поэтому мы храним фазу и дедлайн рядом и читаем оба значения заново. Для сложного матча можно заменить строку фазы на отдельный ModuleScript с конечным автоматом, но источник истины всё равно остаётся на сервере.
Практика: тестируем цикл несколькими игроками
Запускайте тест через вкладку Test с одним сервером и двумя игроками. Уменьшите длительности до 5 и 8 секунд, чтобы быстрее увидеть несколько циклов. Подключите второго игрока посреди раунда и сравните надписи. Затем поставьте breakpoint или искусственную задержку в одном LocalScript: серверная фаза должна завершиться вовремя.
- Создайте RoundGui, TimerLabel и два скрипта в указанных местах Explorer.
- Запустите сервер с двумя игроками и дождитесь перехода «Перерыв → Раунд».
- Сравните секунды в двух окнах: разница не должна накапливаться.
- Подключите нового игрока в середине раунда и проверьте текущую фазу.
- Измените ROUND_SECONDS на 8, затем дождитесь двух полных циклов.
- Добавьте print только на сервере в места старта и финиша и сверьте Output.
Перерыв, раунд и финиш идут в правильном порядке. Несколько клиентов показывают общий дедлайн. Новый игрок не начинает отсчёт с полного времени. Клиент не определяет победителя и не меняет фазу.

Запускайте тест с несколькими клиентами. Пройдите все четыре сценария перед публикацией проекта.
Доводим таймер до настоящей игровой логики
Теперь вставьте реальную игру между сменами фазы. В начале раунда телепортируйте только готовых игроков, очистите предметы прошлого матча и создайте цели. В конце сначала заблокируйте набор очков, затем посчитайте победителя и только после этого покажите «Финиш». Не добавляйте всю механику внутрь publishPhase: пусть функции startRound и finishRound отвечают каждая за один переход.
Игроки получают разные моменты старта, а клиент может подменить состояние.
Задержка планировщика постепенно растягивает раунд.
Для двух чисел достаточно реплицируемых атрибутов.
Разделите startRound, finishRound и публикацию фазы.
Что получится в итоге

Готовый HUD показывает общую фазу и точный остаток, а сервер без участия клиента переключает игровой цикл.
Где должен запускаться цикл раунда?
В Script на сервере, например в ServerScriptService.
Почему храним момент окончания?
Остаток можно пересчитать после любого лага, не накапливая задержку.
Зачем нужны атрибуты?
Они автоматически передают небольшое серверное состояние клиентам, включая поздно вошедших игроков.
Как понять, что проект готов?
Повторите основной сценарий несколько раз, затем проверьте пустой ввод, граничные значения и повторный запуск. Ошибка должна объяснять, что исправить, а не закрывать программу.
Можно ли скопировать код целиком?
Можно использовать его как стартовую точку. После первого запуска измените хотя бы один параметр и объясните себе, где хранятся данные, где выполняется действие и где появляется результат.
Серверные скрипты, циклы и таблицы разбираются в курсе Lua / Luau. Перед раундом игроков можно отправлять через телепорт в Roblox Studio.
Для появления на карте пригодится настройка SpawnLocation, а общий счёт можно вывести через leaderstats. В Кодике удобно сначала разобрать переходы фаз на маленьких значениях.