MAATRIX GAMES / Блог / FiveM: интеграция с картами реального времени, погода и сезоны

FiveM: интеграция с картами реального времени, погода и сезоны

MAATRIX GAMES

Ставишь дефолтный FiveM-сервер — и через день кто-то из игроков пишет в тикет, что у него на экране солнечный полдень, а у соседа по гаражу в это же время идёт дождь ночью. Дело не в багнутом клиенте: без явной синхронизации каждый клиент считает время и погоду сам по себе, и на RP-сервере, где сцены завязаны на «сейчас ночь, все в масках» или «шторм, все прячутся», это ломает атмосферу. Разберём, как привязать время к реальным часам или задать ускоренный цикл, синхронизировать состояние между всеми подключёнными, подтянуть настоящую погоду через внешний API и честно сымитировать смену сезонов там, где GTA V нативно её не поддерживает.

Реальное время или ускоренный цикл — что выбрать

Игровые часы GTA V по умолчанию у каждого клиента идут своим ходом и без вмешательства сервера рано или поздно расходятся. Прежде чем что-то синхронизировать, нужно решить, откуда сервер вообще берёт «эталонное» время.

Привязка к реальному времени — сервер читает системные часы (os.date) и транслирует клиентам время, максимально близкое к реальному, иногда со сдвигом часового пояса под аудиторию сервера. Подходит проектам, где важна предсказуемость: вечером по своим часам игрок видит вечер и в игре, и на это можно завязывать активности вроде ночных патрулей или комендантского часа по расписанию.

Ускоренный цикл — сервер сам крутит игровые сутки с заданной скоростью независимо от реальных часов, например «столько-то игровых минут за одну реальную секунду». Стандартный выбор для RP-серверов, где хочется, чтобы за одну сессию игрок застал и день, и ночь. Конкретное соотношение — настройка проекта, а не величина движка, и её стоит подбирать под длину типичной сессии на своём сервере, а не копировать чужой конфиг вслепую.

-- config/time.lua
Config = Config or {}

Config.Time = {
    mode = 'accelerated',   -- 'realtime' или 'accelerated'
    gameMinutesPerRealSecond = 1.0, -- актуально только для 'accelerated'
    freezeWeather = false,
    startHour = 8,
    syncIntervalMs = 60000, -- как часто рассылать актуальное время клиентам
}

Смешивать оба режима без крайней необходимости не стоит — переключение туда-обратно на живом сервере обычно даёт заметный скачок времени у части игроков, которые были в этот момент в интерьере или катсцене.

Синхронизация времени между всеми клиентами

Ключевая ошибка новичков — вызывать нативы времени локально на каждом клиенте, ожидая, что те как-то сами договорятся. Не договорятся: NETWORK_OVERRIDE_CLOCK_TIME меняет часы только у клиента, где вызвана, а между рассылками игра продолжает тикать свои дефолтные секунды, и клиенты снова расходятся.

Рабочая схема — сервер как единственный источник правды: хранит текущее игровое время, периодически рассылает его всем через TriggerClientEvent, а клиент держит его зафиксированным между рассылками через PAUSE_CLOCK.

-- server/time.lua
local hour, minute = Config.Time.startHour, 0

CreateThread(function()
    while true do
        Wait(Config.Time.syncIntervalMs)
        if Config.Time.mode == 'accelerated' then
            minute = minute + math.floor(Config.Time.syncIntervalMs / 1000 * Config.Time.gameMinutesPerRealSecond)
            hour = hour + math.floor(minute / 60)
            minute = minute % 60
            hour = hour % 24
        else
            hour, minute = tonumber(os.date('%H')), tonumber(os.date('%M'))
        end
        TriggerClientEvent('rp2:syncTime', -1, hour, minute)
    end
end)

-- отдаём текущее состояние сразу при коннекте, не дожидаясь ближайшего тика рассылки
AddEventHandler('playerConnecting', function()
    local src = source
    Wait(0)
    TriggerClientEvent('rp2:syncTime', src, hour, minute)
end)
-- client/time.lua
RegisterNetEvent('rp2:syncTime', function(hour, minute)
    PauseClock(true)
    NetworkOverrideClockTime(hour, minute, 0)
end)

Отдельно стоит поймать playerConnecting (или playerJoining, в зависимости от того, на каком этапе фреймворк уже готов принимать события) — без явной досылки текущего состояния при коннекте игрок, зашедший между двумя плановыми рассылками, несколько секунд-минут видит дефолтное игровое время, что на RP-сервере с активной ночной сценой выглядит как баг. Если сервер только разворачивается с нуля и этого куска инфраструктуры ещё нет, порядок действий с самого начала — в статье про установку FiveM-сервера с нуля.

Поднять сервер FiveM (GTA V) за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

Создать сервер

Погода из внешнего API: как подключить реальную погоду

Погода в FiveM синхронизируется по тому же принципу — сервер авторитетен, клиенты применяют. Разница в том, что источником данных для погоды может быть не игровая логика, а настоящий погодный сервис. Делается это через серверный натив PerformHttpRequest, который FXServer поддерживает из коробки — отдельных зависимостей в fxmanifest.lua, кроме server_script, не требуется.

-- server/weather.lua
local weatherMap = {
    Clear = 'CLEAR',
    Clouds = 'CLOUDS',
    Rain = 'RAIN',
    Thunderstorm = 'THUNDER',
    Snow = 'SNOW',
    Fog = 'FOGGY',
    Mist = 'FOGGY',
}

local function fetchRealWeather(cb)
    PerformHttpRequest('https://api.openweathermap.org/data/2.5/weather?q=' .. Config.Weather.city
        .. '&appid=' .. Config.Weather.apiKey, function(status, body)
        if status ~= 200 or not body then
            cb('CLEAR') -- сервис недоступен — не роняем ресурс, откатываемся на безопасное значение
            return
        end
        local data = json.decode(body)
        local condition = data and data.weather and data.weather[1] and data.weather[1].main
        cb(weatherMap[condition] or 'CLEAR')
    end, 'GET')
end

Здесь важны три вещи, на которых спотыкаются чаще всего:

  • Опрос по таймеру, а не на каждое событие. Погодные API почти всегда лимитируют бесплатный тариф по числу запросов — точные цифры стоит смотреть в актуальной документации выбранного провайдера, они периодически меняются. На практике опроса раз в 5-15 игровых минут достаточно, реальная погода не меняется поминутно.
  • Исходящий доступ в интернет с сервера. PerformHttpRequest выполняется на стороне FXServer, а не клиента, значит серверу нужен исходящий HTTPS. Если хостинг закрывает исходящий трафик по умолчанию на уровне файрвола или security group, запрос просто зависнет в таймауте — это стоит проверить отдельно, прежде чем списывать проблему на кривой скрипт.
  • Фолбэк на случай сбоя API. Сервис может упасть или вернуть не тот формат JSON — код выше на этот случай откатывается на CLEAR, а не роняет ресурс ошибкой.

Маппинг условий погоды на типы GTA (CLEAR, EXTRASUNNY, CLOUDS, OVERCAST, RAIN, THUNDER, CLEARING, NEUTRAL, SNOW, BLIZZARD, SNOWLIGHT, FOGGY, XMAS, HALLOWEEN) — вещь проекта, не движка: разные сервисы называют условия по-разному, и таблицу соответствий нужно сверять с реальным ответом своего провайдера, а не копировать чужую вслепую.

Плавные переходы погоды и защита от рывков

Прямая замена погоды через SET_WEATHER_TYPE_NOW_PERSIST работает мгновенно — было солнечно, стало «щёлк» и гроза. Для одиночного триггера (админ-команда, ивент) это нормально, но для регулярной синхронизации по таймеру резкие рывки погоды каждые несколько минут выглядят дёшево и сбивают с толку игроков в машине, которые не понимают, откуда взялся ливень посреди дороги.

Более щадящий вариант — плавный переход через SET_WEATHER_TYPE_OVERTIME_PERSIST, где вторым параметром задаётся время перехода в секундах:

-- client/weather.lua
local currentWeather = 'CLEAR'

RegisterNetEvent('rp2:syncWeather', function(newWeather)
    if newWeather == currentWeather then return end -- не дёргаем погоду без реальной смены
    SetWeatherTypeOvertimePersist(newWeather, 60.0)
    currentWeather = newWeather
end)

Проверка if newWeather == currentWeather then return end на клиенте не декоративная — без неё каждая плановая рассылка от сервера (даже когда погода объективно не изменилась) заново триггерит переход, и на слабом железе это добавляет лишнюю нагрузку на рендер без всякой пользы. То же самое стоит сделать и на сервере: сравнивать новое значение с последним отправленным и не гонять TriggerClientEvent впустую.

Отдельно нужно закрыть тот же кейс позднего коннекта, что и с временем — новый игрок должен получить текущую погоду сразу при входе, а не ждать следующего планового тика синхронизации.

Влияние на видимость и атмосферу

Погода и время суток — не только картинка, это прямое влияние на геймплей, которое стоит настраивать осознанно, а не оставлять на волю движка.

  • Дальность видимости в тумане и дожде. Сильный туман или ливень заметно режут видимость — усиливает атмосферу, но на серверах с активными перестрелками может превращаться в фрустрацию: игрок не видит противника не потому, что тот спрятался, а потому что погода. Если жалобы повторяются, стоит ограничить пул тяжёлых по видимости типов (FOGGY, THUNDER) более редкой вероятностью, а не убирать совсем.
  • Освещение улиц в грозу. Натив SET_ARTIFICIAL_LIGHTS_STATE позволяет принудительно гасить или зажигать уличное освещение отдельно от погодного цикла — усиливает ощущение шторма или подсвечивает зоны спавна, если ночь выставлена слишком тёмной.
  • Баланс тёмных ночей. Полностью реалистичная беспросветная ночь эффектна на скриншотах, но мешает и игрокам, и админам модерировать происходящее. Многие RP-проекты сознательно занижают глубину ночной темноты модификатором таймцикла на клиенте, а не полагаются на дефолтную яркость движка — это компромисс, который стоит проговорить с командой заранее.
  • Сцепление на мокрой дороге. Дождь и снег влияют на управляемость через хендлинг машин — для гонка-ориентированных фреймворков часть серверов ослабляет эффект правкой handling.meta, для остальных реалистичное скольжение в дождь как раз часть атмосферы.

Если после включения погодных эффектов и синхронизации по расписанию сервер начинает подтормаживать именно при большом онлайне — не всегда виновата сама погода, часто накладывается общая нагрузка от количества стриминг-объектов и скриптов; общий разбор таких проблем — в статье про лаги при большом онлайне на FiveM.

Имитация смены сезонов без нативной поддержки

У GTA V и FiveM нет встроенной системы времён года — карта всегда одна и та же, независимо от календаря. «Сезоны» на RP-серверах — это всегда имитация, собранная из нескольких приёмов, а не отдельная фича движка.

Ограничение пула погоды по календарю. Самый дешёвый по ресурсам способ — просто сузить набор погоды, из которого сервер выбирает случайное значение (или к которому обращается при отсутствии реального API), в зависимости от текущего месяца:

-- config/seasons.lua
Config.SeasonWeatherPool = {
    winter = {'SNOW', 'BLIZZARD', 'SNOWLIGHT', 'FOGGY', 'XMAS'},
    summer = {'CLEAR', 'EXTRASUNNY', 'THUNDER'},
    default = {'CLEAR', 'CLOUDS', 'OVERCAST', 'RAIN', 'CLEARING'},
}

local function getSeasonPool()
    local month = tonumber(os.date('%m'))
    if month == 12 or month == 1 or month == 2 then
        return Config.SeasonWeatherPool.winter
    elseif month >= 6 and month <= 8 then
        return Config.SeasonWeatherPool.summer
    end
    return Config.SeasonWeatherPool.default
end

Эта логика применяется там, где сервер сам выбирает случайную погоду (если реальный API не подключён или временно недоступен) — реальную сгенерированную сезонность API и так учитывает сам, дублировать фильтр поверх него не нужно.

Визуальная подмена снегом и текстурами. Настоящий снежный покров на земле, деревьях и крышах — это уже не про натив погоды, а про карту: отдельные ресурсы подменяют пропсы и текстуры земли на зимние варианты, аналогично тому, как ставятся кастомные MLO-интерьеры. Общая логика структуры ресурса и стрим-файлов та же, что и для обычных карт — подробнее в статье про карты и MLO для FiveM-сервера. Нюанс: снежные текстурные паки почти всегда чисто визуальные, физика и коллизии остаются летними — машина «едет по снегу», но по факту скользит на асфальтовом трении, если отдельно не подключена правка хендлинга под сезон.

Конфликты с уже стоящими картами. Если на сервере уже стоят кастомные MLO или пропсы под конкретный вид локации, зимняя текстурная подмена может визуально конфликтовать с ними в тех же точках — это тот же класс проблем, что и конфликт двух MLO на одной локации, и диагностируется так же: временный stop одного из ресурсов и проверка, воспроизводится ли нестыковка без него.

Поднять сервер FiveM (GTA V) за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

Создать сервер

Частые вопросы

Как быстро протестировать смену дня и ночи, не дожидаясь реального цикла?

На тестовом инстансе временно выставить Config.Time.mode = 'accelerated' с высоким значением gameMinutesPerRealSecond — полный цикл суток пройдёт за пару минут. На проде вернуть нормальное значение отдельным коммитом.

После рестарта ресурса погода и время сбрасываются на дефолт — это нормально?

Нет, значит состояние хранится только в оперативной переменной и теряется при restart. Решение — сохранять последнее значение в KVP (SetResourceKvp) или во внешнем хранилище фреймворка и восстанавливать при старте, а не начинать каждый раз с дефолта.

Обязательно писать синхронизацию с нуля или можно взять готовый ресурс?

Готовые решения для тайм- и погодной синхронизации в комьюнити FiveM есть, включая версии под конкретные фреймворки. Свой скрипт нужен, если требуется нестандартная интеграция — например внешний API реальной погоды или сезонный пул, которые типовой ресурс может не поддерживать.

У части игроков погода "долетает" с задержкой при подключении — почему?

Чаще всего событие рассылается только по таймеру, а playerConnecting не досылает текущее состояние сразу. Игрок заходит между двумя плановыми тиками и какое-то время видит дефолт — фикс тот же, что и для времени: явная отправка состояния сразу на коннект.

Можно ли завязать погоду одновременно на API и на ручное управление админом?

Можно, но нужно разделить приоритеты: ручной триггер выставляет флаг manualOverride на фиксированное время, в течение которого плановый опрос API игнорируется, а по истечении срока синхронизация возвращается к реальным данным — иначе очередной тик молча перезапишет то, что выставил админ.