Разработка игрLua / Luau

Как сделать таймер раунда в Roblox Studio: отсчёт и старт игры

Делаем таймер раунда в Roblox Studio: серверный цикл, перерыв и раунд, синхронный отсчёт через GetServerTimeNow и UI для всех игроков.

Кодик

Автор

6 мин чтения

Таймер раунда должен считать время на сервере, а интерфейс каждого игрока только показывать остаток. Мы соберём цикл «перерыв → раунд → финиш», сохраним фазу и точное время окончания в атрибутах ReplicatedStorage и синхронизируем TextLabel без RemoteEvent на каждую секунду.

Если каждый LocalScript уменьшает своё число раз в секунду, таймеры начинают расходиться после лагов, сворачивания окна или позднего входа игрока. Надёжнее хранить не «осталось 37», а момент окончания раунда. Сервер задаёт одну временную точку, Roblox реплицирует атрибуты, а клиент вычисляет остаток через GetServerTimeNow. Новый игрок сразу читает текущее состояние.

1Сервер решает

RoundLoop переключает фазы и задаёт общее время окончания.

2Атрибуты хранят

RoundPhase и RoundEndsAt автоматически видны клиентам.

3Клиент рисует

LocalScript вычисляет секунды и обновляет один TextLabel.

Из каких частей состоит игровой раунд

Создайте ScreenGui с именем RoundGui в StarterGui, внутри TextLabel с именем TimerLabel и LocalScript. Серверный Script RoundLoop положите в ServerScriptService. Сервер будет менять два атрибута на ReplicatedStorage: строку RoundPhase и число RoundEndsAt. UI никогда не запускает раунд и не решает, кто победил.

ЭлементГде находитсяОтветственность
RoundLoopServerScriptServiceФазы, начало и окончание
RoundPhaseАтрибут ReplicatedStorageНазвание текущей фазы
RoundEndsAtАтрибут ReplicatedStorageОбщий момент окончания
TimerLabelStarterGuiТекст для конкретного игрока

Сервер является источником истины. Клиент получает только данные для отображения, поэтому изменение 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: серверная фаза должна завершиться вовремя.

Проверка своими руками
  1. Создайте RoundGui, TimerLabel и два скрипта в указанных местах Explorer.
  2. Запустите сервер с двумя игроками и дождитесь перехода «Перерыв → Раунд».
  3. Сравните секунды в двух окнах: разница не должна накапливаться.
  4. Подключите нового игрока в середине раунда и проверьте текущую фазу.
  5. Измените ROUND_SECONDS на 8, затем дождитесь двух полных циклов.
  6. Добавьте print только на сервере в места старта и финиша и сверьте Output.
Готово, если выполняются все пункты

Перерыв, раунд и финиш идут в правильном порядке. Несколько клиентов показывают общий дедлайн. Новый игрок не начинает отсчёт с полного времени. Клиент не определяет победителя и не меняет фазу.

Четыре проверки раунда
Запускайте тест с несколькими клиентами. Пройдите все четыре сценария перед публикацией проекта.

Доводим таймер до настоящей игровой логики

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

Таймер запускается в каждом LocalScript

Игроки получают разные моменты старта, а клиент может подменить состояние.

Одна секунда вычитается после wait(1)

Задержка планировщика постепенно растягивает раунд.

RemoteEvent отправляется каждый кадр

Для двух чисел достаточно реплицируемых атрибутов.

Новый раунд стартует до очистки старого

Разделите startRound, finishRound и публикацию фазы.

Сверьтесь с первичным источником. Поведение функций и ограничения примера проверяйте по документацией Roblox по свойствам и атрибутам. Документация особенно важна, когда меняются API, права или правила платформы.

Что получится в итоге

Готовый результат проекта: Как сделать таймер раунда в Roblox Studio: отсчёт и старт игры
Готовый HUD показывает общую фазу и точный остаток, а сервер без участия клиента переключает игровой цикл.

Короткие ответы
Где должен запускаться цикл раунда?

В Script на сервере, например в ServerScriptService.

Почему храним момент окончания?

Остаток можно пересчитать после любого лага, не накапливая задержку.

Зачем нужны атрибуты?

Они автоматически передают небольшое серверное состояние клиентам, включая поздно вошедших игроков.

Как понять, что проект готов?

Повторите основной сценарий несколько раз, затем проверьте пустой ввод, граничные значения и повторный запуск. Ошибка должна объяснять, что исправить, а не закрывать программу.

Можно ли скопировать код целиком?

Можно использовать его как стартовую точку. После первого запуска измените хотя бы один параметр и объясните себе, где хранятся данные, где выполняется действие и где появляется результат.

Превратите отсчёт в настоящий игровой цикл

Серверные скрипты, циклы и таблицы разбираются в курсе Lua / Luau. Перед раундом игроков можно отправлять через телепорт в Roblox Studio.

Для появления на карте пригодится настройка SpawnLocation, а общий счёт можно вывести через leaderstats. В Кодике удобно сначала разобрать переходы фаз на маленьких значениях.