O problema com o clima dessincronizado
Por padrão, GTA V lida com o clima e o horário de forma independente em cada cliente. Isso significa que um jogador pode estar dirigindo sob o sol forte enquanto outro jogador ao lado dele vê uma tempestade. Para servidores de roleplay, isso quebra completamente a imersão. Imagine tentar coordenar uma cena em que um personagem reclama da chuva enquanto a tela de outro jogador mostra o céu limpo. Um sistema sincronizado de clima e tempo garante que cada jogador no servidor experimente o mesmo ambiente no mesmo momento, o que é fundamental para uma interpretação consistente, condições de direção realistas e coordenação de eventos. Neste tutorial, construiremos um sistema completo de sincronização climática com velocidade de tempo configurável, transições sazonais, votação climática e eventos de blecaute.
Gerenciamento do estado meteorológico do lado do servidor
O servidor é a única fonte de verdade sobre o clima e o tempo. Ele mantém o tipo de clima atual, o próximo tipo de clima para transições e o relógio do jogo. Cada cliente sincroniza com este estado quando ingressa e recebe atualizações sempre que o tempo muda. GTA V suporta um conjunto específico de tipos de clima, incluindo CLEAR, EXTRASUNNY, CLOUDS, OVERCAST, RAIN, THUNDER, CLEARING, FOGGY, SMOG, SNOW, SNOWLIGHT, BLIZZARD, e XMAS. Defina regras de transição lógicas para que o clima flua naturalmente, em vez de saltar de uma nevasca para um sol extra sem um estado intermediário.
-- 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},
}Progressão de tempo e loop de sincronização
O sistema de tempo funciona como um loop do lado do servidor que incrementa o relógio do jogo com base na velocidade configurada. Um timeSpeed de 2 significa que cada segundo do mundo real avança o relógio do jogo em 2 minutos. Isso cria um ciclo dia-noite que leva aproximadamente 12 minutos no mundo real durante 24 horas completas de jogo. O servidor transmite a hora atual e o estado do tempo para todos os clientes em intervalos regulares e também envia atualizações imediatas quando um administrador altera o clima ou aciona um evento de blecaute. Novos jogadores que ingressam no meio da sessão recebem o estado atual por meio de um evento de sincronização dedicado para que correspondam imediatamente a todos os outros.
-- 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)Aplicativo meteorológico do lado do cliente
No cliente, você precisa substituir os sistemas nativos de clima e horário do GTA. Os principais nativos são SetWeatherTypeNowPersist para mudanças climáticas instantâneas, SetWeatherTypeTransition para uma combinação suave entre dois estados climáticos e NetworkOverrideClockTime para acertar o relógio do jogo. Você deve chamar NetworkOverrideClockTime a cada quadro para evitar que o jogo volte ao seu próprio cálculo de tempo. Para transições climáticas, o GTA suporta um fator de mistura entre 0,0 e 1,0 que permite interpolar suavemente entre dois estados climáticos durante vários segundos, criando transições realistas onde as nuvens gradualmente aparecem antes do início da chuva.
-- 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 votação meteorológica
Um sistema de votação meteorológica permite que sua comunidade decida o clima democraticamente. Os jogadores podem votar em seu tipo de clima preferido e, após um período de votação, a opção mais popular vence. Isso funciona bem para eventos em que você deseja o envolvimento do jogador, como decidir se um encontro de carros acontece sob céu limpo ou tempestades dramáticas. Limite a votação uma vez por ciclo para evitar spam e exiba as contagens atuais de votos por meio de seu sistema de notificação ou de uma simples sobreposição de NUI. O servidor coleta votos, contabiliza-os quando o período expira e aplica o clima vencedor por meio de uma transição suave para que todos os jogadores experimentem a mudança simultaneamente.
-- 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)
endSistema sazonal e efeitos climáticos na jogabilidade
Um sistema sazonal adiciona uma camada de profundidade que a maioria dos servidores ignora. Acompanhando o calendário do jogo ou mapeando meses do mundo real em estações, você pode avaliar as probabilidades climáticas de acordo. O verão favorece um clima claro e ensolarado com tempestades ocasionais, enquanto o inverno muda o equilíbrio para neve, céu nublado e neblina. As temporadas também podem afetar a mecânica de jogo além do visual. Durante a chuva, você pode reduzir a tração do veículo acionando um modificador de dirigibilidade em todos os veículos ativos. O nevoeiro pode reduzir a distância de renderização dos peds de IA, tornando a jogabilidade furtiva mais viável. A neve pode diminuir ligeiramente a velocidade de movimento do jogador e exigir roupas de inverno para evitar um lento esgotamento da saúde. Esses pequenos toques criam um mundo envolvente que parece vivo e responsivo ao seu ambiente.
Eventos de blackout e controles administrativos
Os eventos de blackout são uma ferramenta poderosa para eventos de servidor e cenários de roleplay. O nativo SetArtificialLightsState desativa todas as fontes de luz artificial no mundo do jogo, mergulhando a cidade na escuridão. Combinado com um estado de tempo nublado ou nebuloso, isso cria um ambiente incrivelmente atmosférico perfeito para eventos de terror, cenários de assalto ou roleplay de sobrevivência. Os comandos administrativos devem fornecer controle total sobre o sistema meteorológico: congelar o tempo, definir horários específicos, forçar tipos de clima, desencadear blecautes e iniciar votações meteorológicas. Proteja esses comandos com permissões Ace para que apenas funcionários autorizados possam modificar o ambiente. Um sistema meteorológico bem projetado se torna uma ferramenta de contar histórias que os administradores usam para definir o clima para cada evento e cenário em seu 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)
