Wróć do bloga
Tutorial8 min czytania

System parkingowy FiveM: Skrypty realistycznego parkowania pojazdów

Dodaj realistyczne parkowanie do swojego miasta FiveM. Parkowanie na ulicy, płatne parkingi, zasady odholowywania i najlepsze skrypty parkingowe, aby zapobiec chaosowi z pojazdami.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Zrozumienie architektury systemu parkingów

System parkowania dodaje głębi i realizmu każdemu serwerowi roleplay FiveM, wyznaczając graczom strefy do zostawiania pojazdów i wprowadzając konsekwencje za łamanie przepisów. U swoich podstaw system parkowania śledzi zajęte miejsca parkingowe, monitoruje czas postoju pojazdów i obsługuje płatności przez parkomaty lub zezwolenia. System składa się z logiki po stronie serwera do śledzenia stanu i przetwarzania płatności, detekcji wjazdu i wyjazdu ze strefy parkowania po stronie klienta oraz opcjonalnego interfejsu NUI wyświetlającego stan parkometru i opcje płatności. Dobrze wdrożony system parkowania integruje się również z istniejącymi skryptami policji i holowników, dzięki czemu nieprawidłowo zaparkowane pojazdy mogą zostać ukarane mandatem lub odholowane na parking policyjny przez uprawnionych graczy.

Definiowanie stref i miejsc parkingowych

Strefy parkowania są definiowane jako wielokątne lub prostopadłościenne obszary na mapie, z których każdy zawiera pojedyncze miejsca parkingowe o określonych współrzędnych i headingach. Użyj pliku konfiguracyjnego do zdefiniowania tych stref, aby administratorzy serwera mogli dodawać nowe obszary parkingowe bez modyfikowania kodu. Każda strefa może mieć inne zasady, takie jak bezpłatne parkowanie, płatne parkometry lub strefy tylko z pozwoleniem. Oto praktyczna struktura konfiguracji stref:

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

Ten vector4 format przechowuje współrzędne x, y, z wraz z kątem heading, zapewniając, że pojazdy są zaparkowane przodem we właściwym kierunku. occupied flaga jest zarządzana w czasie działania przez serwer i nie jest zapisywana między restartami, ponieważ pojazdy są śledzone osobno w bazie danych systemu garażowego. Utrzymuj realistyczną liczbę miejsc dla każdej lokalizacji, ponieważ zbyt wiele miejsc na małym obszarze prowadzi do problemów z przenikaniem się pojazdów.

System czasowy parkomatów

System parkometrów śledzi, jak długo zaparkowany jest każdy pojazd i nalicza odpowiednie opłaty. Gdy gracz parkuje w strefie płatnej, wchodzi w interakcję z parkometrem, aby rozpocząć sesję, wybierając czas trwania i płacąc z góry. Serwer zapisuje czas rozpoczęcia sesji, opłacony czas, numer rejestracyjny i ID strefy. Wątek timera po stronie serwera okresowo sprawdza wszystkie aktywne sesje i oznacza te wygasłe do egzekwowania. Oto podstawowa logika parkometru:

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)

Rozdzielczość timera wynosząca 60 sekund jest wystarczająca do kontroli parkowania bez generowania niepotrzebnego obciążenia procesora. Przechowuj sesje parkometrów w pamięci zamiast w bazie danych, ponieważ są one tymczasowe i nie muszą przetrwać restartu serwera. Po restarcie serwera wszystkie parkometry resetują się, co jest dopuszczalne, ponieważ w większości frameworków pojazdy po restarcie również wracają do garaży.

Wykrywanie pojazdów i przypisywanie miejsc

Wykrywanie momentu wjazdu lub wyjazdu pojazdu z miejsca parkingowego wymaga okresowego sprawdzania odległości po stronie klienta. Użyj sprawdzania opartego na dystansie względem wszystkich miejsc w pobliskich strefach zamiast sprawdzania każdego miejsca na całej mapie. Uruchamiaj pętlę wykrywania tylko wtedy, gdy gracz znajduje się w pojeździe i w pobliżu skonfigurowanej strefy parkingowej. Pozwala to utrzymać minimalne zużycie procesora podczas normalnej rozgrywki:

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

Sprawdzanie prędkości na poziomie 0.5 sprawia, że komunikat o parkowaniu pojawia się tylko wtedy, gdy pojazd prawie stoi, co zapobiega migotaniu interfejsu UI, gdy gracz przejeżdża przez parking. Adaptacyjny interwał uśpienia wynoszący 200ms w pobliżu stref w porównaniu do 1000ms w innych miejscach równoważy responsywność z wydajnością. W przypadku serwerów z wieloma strefami parkowania warto rozważyć użycie spatial hash lub quadtree, aby uniknąć sprawdzania każdej strefy przy każdym ticku.

System mandatów parkingowych i egzekwowania opłat

Gdy czas parkometru upłynie lub pojazd jest zaparkowany nielegalnie, system wystawia mandat przypisany do właściciela pojazdu. Mandaty są przechowywane w tabeli bazy danych i wyświetlane, gdy właściciel zaloguje się ponownie lub odbierze swój pojazd. Gracze policji mogą również ręcznie wystawiać mandaty za parkowanie za pomocą komendy lub interakcji target. Wpis mandatu zawiera tablicę rejestracyjną, kwotę, powód wystawienia oraz znacznik czasu:

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)

Zintegruj system mandatów z procedurą odbierania pojazdów, aby gracze musieli opłacić zaległe mandaty przed wyciągnięciem pojazdu z garażu. Tworzy to naturalną pętlę egzekwowania prawa, w której ignorowanie zasad parkowania wiąże się z realnymi konsekwencjami w grze. Rozważ dodanie 5 do 10 minut okresu karencji po wygaśnięciu parkometru przed wystawieniem mandatu, ponieważ odzwierciedla to rzeczywiste egzekwowanie przepisów i zmniejsza frustrację graczy spowodowaną mandatami wystawianymi co do sekundy.

Wyświetlacz taksometru i interfejs płatności NUI

Interfejs NUI parkometru powinien być przejrzysty i czytelny, pokazując aktualną nazwę strefy, stawkę za godzinę, pozostały czas w przypadku wcześniejszego opłacenia oraz przyciski wyboru czasu trwania i płatności. Wyświetlaj timer odliczający co sekundę, gdy parkometr jest aktywny, aby gracz dokładnie wiedział, ile czasu mu zostało. Zastosuj kompaktowy design, który nie zasłania widoku gry, na wzór rzeczywistego ekranu parkometru. Zastosuj animację wyświetlacza timera ze zmianą koloru z zielonego na żółty i czerwony w miarę upływu czasu, co stanowi intuicyjną wskazówkę wizualną bez konieczności odczytywania dokładnej liczby pozostałych minut. Zachowaj lekkość NUI, używając czystego JavaScriptu (vanilla JS) i minimalistycznego CSS, unikając ciężkich frameworków, które wydłużają czas ładowania prostego interfejsu.

Integracja holowania i porzucone pojazdy

Połącz swój system parkowania z mechaniką holowania, aby wygasłe lub nieprawidłowo zaparkowane pojazdy mogły być odholowywane na parking impound. Gdy pojazd narusza przepisy dłużej niż skonfigurowany próg, oznacz go w stanie serwera jako zdatny do odholowania. Gracze pracujący jako holownicy widzą wtedy te pojazdy wyróżnione na mapie i zarabiają na ich odholowaniu. Proces holowania powinien zaktualizować stan pojazdu w bazie danych z "out" na "impounded" i zapisać powód odholowania jako naruszenie zasad parkowania. W przypadku porzuconych pojazdów, które stoją na miejscach parkingowych przez dłuższy czas bez żadnej interakcji ze strony graczy, wdróż procedurę automatycznego czyszczenia, która przenosi je na impound po konfigurowalnym czasie, zwalniając miejsca dla aktywnych graczy. Zapobiega to zapełnianiu parkingów pojazdami graczy, którzy nie logowali się od wielu dni lub tygodni.

Kwestie wydajności i skalowalności

Systemy parkingowe działają w ciągłych pętlach wykrywania, które mogą wpłynąć na wydajność, jeśli nie zostaną starannie zoptymalizowane. Wykrywanie pojazdów po stronie klienta powinno aktywować się tylko dla stref w zasięgu renderowania, a wątek sprawdzania parkomatów po stronie serwera powinien przetwarzać wsadowo wszystkie aktywne sesje zamiast tworzyć osobne timery dla każdego pojazdu. Zaindeksuj tabelę bazy danych mandatów parkingowych w kolumnach plate i citizenid, aby zapewnić szybkie wyszukiwanie, gdy gracze odbierają pojazdy lub gdy administratorzy przeglądają historię mandatów. W przypadku serwerów z dziesiątkami stref parkowania i setkami pojazdów jednocześnie, rozważ buforowanie danych stref w tabeli Lua zamiast odczytywania ich z configu przy każdym sprawdzeniu, oraz korzystaj z funkcji natywnej GetClosestVehicle oszczędnie, ponieważ skanuje wszystkie pojazdy w zasięgu. Profiluj swój resource parkowania za pomocą wbudowanego profilera FiveM pod szczytowym obciążeniem, aby zidentyfikować wąskie gardła, i pamiętaj, że system parkowania powinien zużywać mniej niż 0.1ms czasu server tick, aby nie wpływać negatywnie na ogólną wydajność serwera.

Pasujący skrypt

Gotowe na twój serwer, aktualizacje w zestawie.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.