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.