返回博客
Tutorial4 分钟阅读

FiveM 天气同步:动态天气与时间脚本

在您的FiveM服务器上同步天气和时间。动态循环、雷电、降雪、季节包以及设置氛围的最佳天气脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

不同步天气的问题

默认情况下,GTA V在每个客户端独立处理天气和时间。这意味着一个玩家可能在阳光明媚中驾驶,而旁边的玩家却看到雷暴。对于角色扮演服务器,这完全破坏沉浸感。想象协调一个场景,一个角色抱怨下雨,而另一个玩家屏幕显示晴天。同步天气和时间系统确保服务器上每个玩家在同一时刻体验相同环境,这对一致的角色扮演、真实驾驶条件和事件协调至关重要。本教程将构建一个完整的天气同步系统,具备可配置的时间速度、季节过渡、天气投票和停电事件。

服务器端天气状态管理

服务器是天气和时间的唯一真实来源。它维护当前天气类型、下一天气类型以便过渡,以及游戏内时钟。每个客户端加入时同步此状态,并在天气变化时接收更新。GTA V支持特定的天气类型,包括 CLEAR, EXTRASUNNY, CLOUDS, OVERCAST, RAIN, THUNDER, CLEARING, FOGGY, SMOG, SNOW, SNOWLIGHT, BLIZZARD,和 XMAS. 定义逻辑过渡规则,使天气自然流转,而不是从暴风雪突然跳到极度晴朗而无中间状态。

-- 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},
}

时间进度与同步循环

时间系统作为服务器端循环运行,根据配置速度递增游戏内时钟。一个 timeSpeed 2的含义是每真实秒钟游戏内时钟前进2分钟。这创建了一个昼夜循环,完整24小时游戏时间约需12分钟真实时间。服务器定期向所有客户端广播当前时间和天气状态,并在管理员更改天气或触发停电事件时立即推送更新。中途加入的新玩家通过专门的同步事件接收当前状态,确保与其他玩家同步。

-- 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)

客户端天气应用

客户端需要覆盖GTA的原生天气和时间系统。关键的native函数是 SetWeatherTypeNowPersist 用于即时天气变化, SetWeatherTypeTransition 用于两个天气状态之间的平滑混合,以及 NetworkOverrideClockTime 用于设置游戏内时钟。您必须调用 NetworkOverrideClockTime 每帧调用以防止游戏恢复自身时间计算。对于天气过渡,GTA支持0.0到1.0之间的混合因子,允许您在几秒内平滑插值两个天气状态,创造云层逐渐聚集后开始下雨的逼真过渡。

-- 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)

天气投票系统

天气投票系统让社区民主决定天气。玩家可投票选择偏好天气,投票期结束后最受欢迎选项获胜。适合需要玩家参与的活动,如决定车聚是在晴天还是暴风雨下举行。限制每周期投票一次防止刷票,通过通知系统或简单NUI覆盖显示当前票数。服务器收集投票,期满统计并通过平滑过渡应用获胜天气,确保所有玩家同时体验天气变化。

-- 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

对游戏玩法有影响的季节系统和天气效果

季节系统为大多数服务器忽视的内容增加深度。通过跟踪游戏内日历或将现实月份映射为季节,可相应调整天气概率。夏季偏好晴朗和偶尔雷暴,冬季则偏向降雪、多云和雾。季节还可影响游戏机制,不仅仅是视觉效果。雨天可通过触发所有活动车辆的操控修改器减少车辆抓地力。雾天降低 AI 行人的渲染距离,使潜行玩法更可行。雪天稍微降低玩家移动速度,需穿冬装以避免缓慢生命值流失。这些细节创造了一个生动且对环境响应的沉浸世界。

停电事件与管理员控制

停电事件是服务器活动和角色扮演场景的强大工具。 SetArtificialLightsState native 禁用游戏世界中的所有人造光源,使城市陷入黑暗。结合阴天或雾天天气状态,营造极具氛围的环境,适合恐怖事件、抢劫场景或生存角色扮演。管理员命令应提供对天气系统的完全控制:冻结时间、设置特定时间、强制天气类型、触发停电和启动天气投票。用 ace 权限保护这些命令,确保只有授权人员能修改环境。设计良好的天气系统成为管理员为服务器上的每个事件和场景设定氛围的叙事工具。

-- 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)

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。