>
Tutorial 2026-04-29

Desenvolvimento de Sistemas de Estacionamento e Parquímetros em FiveM

OntelMonke

OntelMonke

Admin & Developer na Agency Scripts

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.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.