Volver al blog
Tutorial8 min de lectura

Sincronización del clima en FiveM: scripts de clima y hora dinámicos

Sincroniza el clima y la hora en tu servidor de FiveM. Ciclos dinámicos, tormentas, nieve, paquetes de temporada y los mejores scripts para crear ambiente.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

El problema del clima desincronizado

Por defecto, GTA V gestiona el clima y la hora por separado en cada cliente. Esto significa que un jugador puede conducir bajo un sol radiante mientras otro, justo a su lado, ve una tormenta eléctrica. En los servidores de roleplay, esto rompe por completo la inmersión. Imagina intentar coordinar una escena en la que un personaje se queja de la lluvia mientras en la pantalla de otro jugador hay un cielo despejado. Un sistema sincronizado de clima y hora garantiza que todos los jugadores del servidor experimenten el mismo entorno al mismo tiempo, algo fundamental para lograr un roleplay coherente, condiciones de conducción realistas y una buena coordinación de eventos. En este tutorial, crearemos un sistema completo de sincronización del clima con velocidad del tiempo configurable, cambios estacionales, votaciones sobre el clima y eventos de apagón.

Gestión del estado del clima del lado del servidor

El servidor es la única fuente de verdad para el clima y la hora. Mantiene el tipo de clima actual, el siguiente tipo de clima para las transiciones y el reloj del juego. Cada cliente se sincroniza con este estado al conectarse y recibe actualizaciones cada vez que cambia el clima. GTA V admite un conjunto específico de tipos de clima, entre ellos CLEAR, EXTRASUNNY, CLOUDS, OVERCAST, RAIN, THUNDER, CLEARING, FOGGY, SMOG, SNOW, SNOWLIGHT, BLIZZARD, y XMASDefine reglas lógicas de transición para que el clima evolucione de forma natural, en lugar de pasar de una ventisca a un sol radiante sin un estado intermedio.

-- server.lua
local Config = {
    timeSpeed    = 2,        -- how many in-game minutes per real second
    syncInterval = 30000,    -- full sync broadcast every 30s
    autoChange   = true,     -- automatic weather cycling
    changeCooldown = 600,    -- seconds between automatic changes
}

local State = {
    weather     = 'CLEAR',
    nextWeather = 'CLOUDS',
    hour        = 12,
    minute      = 0,
    frozen      = false,
    blackout    = false,
    season      = 'summer',
}

local weatherTransitions = {
    EXTRASUNNY = {'CLEAR', 'CLOUDS'},
    CLEAR      = {'EXTRASUNNY', 'CLOUDS', 'OVERCAST'},
    CLOUDS     = {'CLEAR', 'OVERCAST', 'RAIN'},
    OVERCAST   = {'CLOUDS', 'RAIN', 'THUNDER', 'FOGGY'},
    RAIN       = {'OVERCAST', 'THUNDER', 'CLEARING'},
    THUNDER    = {'RAIN', 'OVERCAST'},
    CLEARING   = {'CLEAR', 'CLOUDS'},
    FOGGY      = {'CLEAR', 'CLOUDS', 'SMOG'},
    SMOG       = {'FOGGY', 'CLEAR'},
}

local seasonWeights = {
    summer = {EXTRASUNNY = 30, CLEAR = 30, CLOUDS = 20, RAIN = 10, THUNDER = 5, FOGGY = 5},
    winter = {CLOUDS = 20, OVERCAST = 20, SNOW = 25, SNOWLIGHT = 15, BLIZZARD = 10, FOGGY = 10},
    spring = {CLEAR = 25, CLOUDS = 25, RAIN = 20, CLEARING = 15, FOGGY = 10, OVERCAST = 5},
    autumn = {CLOUDS = 25, OVERCAST = 25, RAIN = 20, FOGGY = 15, CLEAR = 10, SMOG = 5},
}

Progresión de la hora y bucle de sincronización

El sistema de hora funciona como un bucle del lado del servidor que adelanta el reloj del juego según la velocidad configurada. Un timeSpeed de 2 significa que cada segundo en tiempo real adelanta el reloj del juego 2 minutos. Esto crea un ciclo de día y noche que dura aproximadamente 12 minutos en tiempo real por cada 24 horas dentro del juego. El servidor transmite la hora actual y el estado del clima a todos los clientes en intervalos regulares, y también envía actualizaciones inmediatas cuando un administrador cambia el clima o activa un apagón. Los nuevos jugadores que se conectan a mitad de la sesión reciben el estado actual mediante un evento de sincronización dedicado, para que coincidan de inmediato con los demás.

-- Time progression loop (server.lua continued)
CreateThread(function()
    while true do
        Wait(1000)
        if not State.frozen then
            State.minute = State.minute + Config.timeSpeed
            if State.minute >= 60 then
                State.hour = State.hour + math.floor(State.minute / 60)
                State.minute = State.minute % 60
            end
            if State.hour >= 24 then
                State.hour = State.hour % 24
            end
        end
    end
end)

-- Periodic full sync
CreateThread(function()
    while true do
        Wait(Config.syncInterval)
        TriggerClientEvent('weather:fullSync', -1, State)
    end
end)

-- Sync new players on join
AddEventHandler('playerJoining', function()
    local src = source
    Wait(2000)
    TriggerClientEvent('weather:fullSync', src, State)
end)

Aplicación del clima del lado del cliente

En el cliente, tienes que anular los sistemas nativos de clima y hora de GTA. Los natives clave son SetWeatherTypeNowPersist para cambiar el clima de inmediato, SetWeatherTypeTransition para mezclar suavemente dos estados del clima y NetworkOverrideClockTime para establecer el reloj del juego. Debes llamar a NetworkOverrideClockTime en cada frame para evitar que el juego vuelva a calcular la hora por su cuenta. Para las transiciones del clima, GTA admite un factor de mezcla entre 0.0 y 1.0 que permite interpolar suavemente entre dos estados del clima durante varios segundos, creando transiciones realistas en las que las nubes se van acercando antes de que empiece a llover.

-- client.lua
local currentWeather = 'CLEAR'
local nextWeather    = 'CLEAR'
local weatherBlend   = 0.0
local hour, minute   = 12, 0
local blackout       = false

RegisterNetEvent('weather:fullSync', function(state)
    currentWeather = state.weather
    nextWeather    = state.nextWeather or state.weather
    hour           = state.hour
    minute         = state.minute
    blackout       = state.blackout or false
    applyWeather()
end)

function applyWeather()
    ClearOverrideWeather()
    ClearWeatherTypePersist()
    SetWeatherTypeNowPersist(currentWeather)
    if currentWeather ~= nextWeather then
        SetWeatherTypeTransition(
            GetHashKey(currentWeather),
            GetHashKey(nextWeather),
            weatherBlend
        )
    end
    if blackout then
        SetArtificialLightsState(true)
        SetArtificialLightsStateAffectsVehicles(false)
    else
        SetArtificialLightsState(false)
    end
end

-- Override clock every frame
CreateThread(function()
    while true do
        Wait(0)
        NetworkOverrideClockTime(hour, minute, 0)
    end
end)

Sistema de votación del clima

Un sistema de votación del clima permite que tu comunidad decida el clima de forma democrática. Los jugadores pueden votar por su tipo de clima preferido y, al terminar el periodo de votación, gana la opción más popular. Funciona bien para eventos en los que quieres fomentar la participación de los jugadores, como decidir si una reunión de coches se celebra con el cielo despejado o en medio de tormentas eléctricas intensas. Limita la votación a una vez por ciclo para evitar el spam y muestra el recuento de votos actual mediante tu sistema de notificaciones o una superposición NUI sencilla. El servidor recopila los votos, los cuenta cuando termina el periodo y aplica el clima ganador mediante una transición suave para que todos los jugadores experimenten el cambio a la vez.

-- Weather voting (server.lua)
local votes = {}
local voteCooldown = {}
local votingOpen = false
local voteOptions = {'CLEAR', 'RAIN', 'THUNDER', 'FOGGY', 'SNOW'}

RegisterCommand('voteweather', function(source, args)
    if not votingOpen then
        TriggerClientEvent('chat:addMessage', source, {args = {'Weather', 'No vote is currently active.'}})
        return
    end
    if voteCooldown[source] then
        TriggerClientEvent('chat:addMessage', source, {args = {'Weather', 'You already voted this round.'}})
        return
    end
    local choice = string.upper(args[1] or '')
    local valid = false
    for _, opt in ipairs(voteOptions) do
        if opt == choice then valid = true break end
    end
    if not valid then
        TriggerClientEvent('chat:addMessage', source, {
            args = {'Weather', 'Options: ' .. table.concat(voteOptions, ', ')}
        })
        return
    end
    votes[choice] = (votes[choice] or 0) + 1
    voteCooldown[source] = true
    TriggerClientEvent('chat:addMessage', -1, {
        args = {'Weather', GetPlayerName(source) .. ' voted for ' .. choice}
    })
end, false)

function startWeatherVote()
    votes = {}
    voteCooldown = {}
    votingOpen = true
    TriggerClientEvent('chat:addMessage', -1, {
        args = {'Weather', 'Weather vote started! Use /voteweather [type]. Options: ' .. table.concat(voteOptions, ', ')}
    })
    SetTimeout(60000, function()
        votingOpen = false
        local winner, maxVotes = 'CLEAR', 0
        for weather, count in pairs(votes) do
            if count > maxVotes then winner = weather; maxVotes = count end
        end
        transitionWeather(winner)
        TriggerClientEvent('chat:addMessage', -1, {
            args = {'Weather', 'Vote ended! Weather changing to: ' .. winner}
        })
    end)
end

Sistema estacional y efectos del clima en la jugabilidad

Un sistema estacional añade una capa de profundidad que la mayoría de los servidores pasan por alto. Al hacer un seguimiento del calendario del juego o asignar estaciones a los meses del mundo real, puedes ajustar las probabilidades del clima según corresponda. En verano predominan los cielos despejados y soleados, con tormentas eléctricas ocasionales, mientras que en invierno aumenta la probabilidad de nieve, cielos nublados y niebla. Las estaciones también pueden afectar a las mecánicas de juego, más allá del aspecto visual. Cuando llueve, podrías reducir la tracción de los vehículos aplicando un modificador de manejo a todos los vehículos activos. La niebla podría reducir la distancia de renderizado de los peds de la IA, haciendo más viable la jugabilidad de sigilo. La nieve podría reducir ligeramente la velocidad de movimiento de los jugadores y exigir ropa de invierno para evitar una pérdida gradual de salud. Estos pequeños detalles crean un mundo inmersivo que parece vivo y reacciona a su entorno.

Apagones y controles de administrador

Los apagones son una herramienta eficaz para eventos del servidor y situaciones de roleplay. El native SetArtificialLightsState desactiva todas las fuentes de luz artificial del mundo del juego y sume la ciudad en la oscuridad. Combinado con un estado del clima nublado o con niebla, crea un ambiente increíble, perfecto para eventos de terror, situaciones de atracos o roleplay de supervivencia. Los comandos de administrador deben ofrecer control total sobre el sistema de clima: congelar la hora, establecer horas específicas, forzar tipos de clima, activar apagones e iniciar votaciones del clima. Protege estos comandos con permisos ace para que solo el personal autorizado pueda modificar el entorno. Un sistema de clima bien diseñado se convierte en una herramienta narrativa que los administradores usan para crear el ambiente de cada evento y situación en tu servidor.

-- Admin commands (server.lua)
RegisterCommand('setweather', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.setweather') then return end
    local weather = string.upper(args[1] or 'CLEAR')
    transitionWeather(weather)
    TriggerClientEvent('chat:addMessage', -1, {
        args = {'Admin', 'Weather changed to ' .. weather}
    })
end, true)

RegisterCommand('settime', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.settime') then return end
    State.hour   = tonumber(args[1]) or 12
    State.minute = tonumber(args[2]) or 0
    TriggerClientEvent('weather:fullSync', -1, State)
end, true)

RegisterCommand('blackout', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.blackout') then return end
    State.blackout = not State.blackout
    TriggerClientEvent('weather:fullSync', -1, State)
    TriggerClientEvent('chat:addMessage', -1, {
        args = {'Admin', 'Blackout ' .. (State.blackout and 'enabled' or 'disabled')}
    })
end, true)

RegisterCommand('freezetime', function(source, args)
    if source > 0 and not IsPlayerAceAllowed(source, 'command.freezetime') then return end
    State.frozen = not State.frozen
    TriggerClientEvent('chat:addMessage', -1, {
        args = {'Admin', 'Time ' .. (State.frozen and 'frozen' or 'unfrozen')}
    })
end, true)

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.