Compreendendo a arquitetura do sistema de estacionamento
Um sistema de estacionamento adiciona profundidade e realismo a qualquer servidor de RPG FiveM, dando aos jogadores áreas designadas para deixar seus veículos com consequências por ignorarem as regras. Na sua base, um sistema de estacionamento rastreia quais vagas estão ocupadas, monitora há quanto tempo os veículos estão estacionados e faz o pagamento por meio de parquímetros ou licenças. O sistema consiste em lógica do lado do servidor para rastrear o estado e processar pagamentos, detecção do lado do cliente para entrada e saída de zonas de estacionamento e uma interface NUI opcional para exibir o status do medidor e opções de pagamento. Um sistema de estacionamento bem implementado também se integra aos seus scripts policiais e de reboque existentes, para que os veículos estacionados ilegalmente possam ser multados ou apreendidos por jogadores autorizados.
Definição de zonas e vagas de estacionamento
As zonas de estacionamento são definidas como áreas poligonais ou em forma de caixa no mapa, cada uma contendo vagas de estacionamento individuais com coordenadas e rumos específicos. Use um arquivo de configuração para definir essas zonas para que os administradores do servidor possam adicionar novas áreas de estacionamento sem modificar o código. Cada zona pode ter regras diferentes, como estacionamento gratuito, parquímetro ou áreas somente permitidas. Aqui está uma estrutura prática de configuração de zona:
Config.ParkingZones = {
['pillbox_lot'] = {
label = 'Pillbox Medical Parking',
type = 'metered', -- 'free', 'metered', 'permit'
rate = 50, -- $ per hour (metered only)
maxTime = 120, -- minutes max parking
blip = {sprite = 357, color = 3, scale = 0.6},
spots = {
{coords = vector4(310.5, -590.2, 43.3, 70.0), occupied = false},
{coords = vector4(313.8, -591.6, 43.3, 70.0), occupied = false},
{coords = vector4(317.1, -593.0, 43.3, 70.0), occupied = false},
{coords = vector4(320.4, -594.4, 43.3, 70.0), occupied = false},
},
fineAmount = 250, -- fine for overtime or illegal parking
},
['legion_street'] = {
label = 'Legion Square Parking',
type = 'metered',
rate = 75,
maxTime = 60,
blip = {sprite = 357, color = 3, scale = 0.6},
spots = {
{coords = vector4(215.2, -810.5, 30.7, 160.0), occupied = false},
{coords = vector4(212.6, -810.5, 30.7, 160.0), occupied = false},
},
fineAmount = 350,
},
}O formato vector4 armazena as coordenadas x, y, z junto com o ângulo de direção, garantindo que os veículos estejam estacionados na direção correta. O sinalizador occupied é gerenciado em tempo de execução pelo servidor e não é salvo entre reinicializações, pois os veículos são rastreados separadamente através do banco de dados do sistema de garagem. Mantenha a contagem de pontos realista para cada local, pois ter muitos pontos em uma área pequena pode causar problemas de recorte de veículos.
Sistema de temporizador de parquímetro
O sistema de medidor rastreia quanto tempo cada veículo ficou estacionado e cobra de acordo. Quando um jogador estaciona em uma zona com taxímetro, ele interage com um medidor para iniciar a sessão, selecionando uma duração e pagando adiantado. O servidor armazena o horário de início da sessão, duração paga, número da placa e ID da zona. Um thread de timer do lado do servidor verifica periodicamente todas as sessões ativas do medidor e marca as expiradas para aplicação. Aqui está a lógica principal do medidor:
local ActiveMeters = {} -- keyed by plate
RegisterNetEvent('parking:server:startMeter', function(plate, zoneId, duration)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local zone = Config.ParkingZones[zoneId]
if not zone or zone.type ~= 'metered' then return end
-- Calculate cost based on duration in minutes
local hours = duration / 60
local cost = math.ceil(hours * zone.rate)
if Player.Functions.RemoveMoney('cash', cost, 'parking-meter') then
ActiveMeters[plate] = {
zone = zoneId,
startTime = os.time(),
paidUntil = os.time() + (duration * 60),
plate = plate,
owner = Player.PlayerData.citizenid,
}
TriggerClientEvent('parking:client:meterStarted', src, duration, cost)
else
TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
end
end)
-- Check for expired meters every 60 seconds
CreateThread(function()
while true do
Wait(60000)
local now = os.time()
for plate, meter in pairs(ActiveMeters) do
if now > meter.paidUntil then
-- Meter expired, issue fine
IssueParkingFine(plate, meter.zone, meter.owner)
ActiveMeters[plate] = nil
end
end
end
end)A resolução do temporizador de 60 segundos é suficiente para controlar o estacionamento sem criar carga desnecessária na CPU. Armazene as sessões do medidor na memória em vez de no banco de dados, pois elas são temporárias e não precisam sobreviver às reinicializações do servidor. Quando o servidor é reiniciado, todos os medidores são reiniciados, o que é aceitável porque os veículos também retornam às garagens ao serem reiniciados na maioria dos frameworks.
Detecção de veículos e atribuição de pontos
Detectar quando um veículo entra ou sai de uma vaga de estacionamento requer verificações periódicas de proximidade por parte do cliente. Use uma verificação baseada na distância em todos os pontos nas zonas próximas, em vez de verificar todos os pontos do mapa inteiro. Execute o loop de detecção apenas quando o jogador estiver em um veículo e próximo a uma zona de estacionamento configurada. Isso mantém o uso da CPU mínimo durante o jogo normal:
-- client/detection.lua
local nearestZone = nil
local currentSpot = nil
CreateThread(function()
while true do
local sleep = 1000
local ped = PlayerPedId()
local inVehicle = IsPedInAnyVehicle(ped, false)
if inVehicle then
local veh = GetVehiclePedIsIn(ped, false)
local vehCoords = GetEntityCoords(veh)
for zoneId, zone in pairs(Config.ParkingZones) do
local firstSpot = zone.spots[1]
local zoneDist = #(vehCoords - vector3(firstSpot.coords.x,
firstSpot.coords.y, firstSpot.coords.z))
if zoneDist < 50.0 then
sleep = 200
nearestZone = zoneId
for i, spot in ipairs(zone.spots) do
local spotPos = vector3(spot.coords.x, spot.coords.y, spot.coords.z)
local dist = #(vehCoords - spotPos)
if dist < 3.0 then
local speed = GetEntitySpeed(veh)
if speed < 0.5 then
currentSpot = {zone = zoneId, index = i}
ShowParkingPrompt(zone)
end
end
end
break
end
end
else
nearestZone = nil
currentSpot = nil
end
Wait(sleep)
end
end)A verificação de velocidade de 0,5 garante que o aviso de estacionamento só apareça quando o veículo estiver quase parado, evitando que a IU pisque enquanto o jogador dirige muito. O intervalo de sono adaptativo de 200 ms perto de zonas versus 1.000 ms em outros lugares equilibra a capacidade de resposta com o desempenho. Para servidores com muitas zonas de estacionamento, considere usar um hash espacial ou quadtree para evitar a iteração em todas as zonas em cada tick.
Multa de estacionamento e sistema de fiscalização
Quando o taxímetro expira ou um veículo é estacionado ilegalmente, o sistema emite uma multa vinculada ao proprietário do veículo. As multas são armazenadas em uma tabela de banco de dados e exibidas na próxima vez que o proprietário fizer login ou recuperar seu veículo. Os jogadores policiais também podem emitir multas de estacionamento manualmente usando um comando ou interação de alvo. O registro da multa inclui placa, valor, motivo da emissão e carimbo de data/hora:
function IssueParkingFine(plate, zoneId, citizenid)
local zone = Config.ParkingZones[zoneId]
local amount = zone and zone.fineAmount or 250
MySQL.insert([[
INSERT INTO parking_fines (plate, citizenid, amount, reason, zone, issued_at)
VALUES (?, ?, ?, ?, ?, NOW())
]], {plate, citizenid, amount, 'Expired parking meter', zoneId})
-- Notify owner if online
local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
if Player then
TriggerClientEvent('QBCore:Notify', Player.PlayerData.source,
'Parking fine issued: $' .. amount, 'error')
end
end
-- Police manual ticket command
RegisterNetEvent('parking:server:issueTicket', function(plate, reason)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
if Player.PlayerData.job.name ~= 'police' then return end
local vehicle = MySQL.query.await(
'SELECT citizenid FROM player_vehicles WHERE plate = ?', {plate}
)
if vehicle and vehicle[1] then
IssueParkingFine(plate, 'manual', vehicle[1].citizenid)
TriggerClientEvent('QBCore:Notify', src, 'Ticket issued for ' .. plate, 'success')
end
end)Integre o sistema de multas ao seu processo de recuperação de veículos para que os jogadores paguem as multas pendentes antes de retirar o veículo da garagem. Isso cria um ciclo natural de fiscalização onde ignorar as regras de estacionamento tem consequências reais no jogo. Considere adicionar um período de carência de cinco a dez minutos após o vencimento do medidor antes de emitir a multa, pois isso reflete a fiscalização de estacionamento no mundo real e reduz a frustração do jogador com multas em tempo perfeito.
Exibição do medidor NUI e interface de pagamento
O NUI do parquímetro deve ser limpo e informativo, mostrando o nome da zona atual, tarifa por hora, tempo restante se já pago e botões para selecionar a duração e efetuar o pagamento. Exibe um cronômetro de contagem regressiva que é atualizado a cada segundo quando um medidor está ativo para que o jogador saiba exatamente quanto tempo resta. Use um design compacto que não obstrua a visualização do jogo, semelhante a uma tela de parquímetro do mundo real. Anime a exibição do cronômetro com uma transição de cores de verde para amarelo e vermelho conforme o tempo se esgota, fornecendo uma dica visual intuitiva sem exigir que o jogador leia os minutos exatos restantes. Mantenha o NUI leve usando JavaScript básico e CSS mínimo, evitando estruturas pesadas que adicionam tempo de carregamento para uma interface simples.
Integração de reboque e veículos abandonados
Conecte seu sistema de estacionamento a um mecânico de reboque para que veículos vencidos ou estacionados ilegalmente possam ser rebocados para o estacionamento apreendido. Quando um veículo estiver em violação por mais tempo que o limite configurado, marque-o como rebocável no estado do servidor. Os jogadores que trabalham com guinchos podem então ver esses veículos destacados em seus mapas e receber pagamento por rebocá-los. O processo de reboque deve atualizar o estado do veículo no banco de dados de “fora” para “apreendido” e registrar o motivo da apreensão como uma violação de estacionamento. Para veículos abandonados que ficaram parados em locais por longos períodos sem qualquer interação do jogador, implemente uma rotina de limpeza automática que os mova para apreensão após um tempo limite configurável, liberando locais para jogadores ativos. Isso evita que os estacionamentos fiquem lotados de veículos pertencentes a jogadores que não fazem login há dias ou semanas.
Considerações sobre desempenho e escalabilidade
Os sistemas de estacionamento executam ciclos de detecção contínuos que podem afetar o desempenho se não forem otimizados cuidadosamente. A detecção de veículos no lado do cliente deve ser ativada apenas para zonas dentro da distância de renderização, e o thread de verificação do medidor no lado do servidor deve processar em lote todas as sessões ativas, em vez de criar temporizadores individuais por veículo. Indexe a tabela do banco de dados de multas de estacionamento nas colunas Plate e Citizenid para garantir pesquisas rápidas quando os jogadores recuperam veículos ou quando os administradores consultam o histórico de multas. Para servidores com dezenas de zonas de estacionamento e centenas de veículos simultâneos, considere armazenar em cache os dados da zona em uma tabela Lua em vez de ler a configuração em cada verificação e use a função nativa GetClosestVehicle com moderação, pois ela verifica todos os veículos ao alcance. Crie o perfil do seu recurso de estacionamento com o criador de perfil integrado FiveM sob carga de pico para identificar quaisquer pontos de acesso e lembre-se de que um sistema de estacionamento deve usar menos de 0,1 ms de tempo de tick do servidor para evitar afetar o desempenho geral do servidor.

