Wróć do bloga
Tutorial8 min czytania

System garaży FiveM: Przechowuj, spawnuj i odholowuj pojazdy

Zaprojektuj niezawodny system garaży dla FiveM. Garaże prywatne i frakcyjne, parking depozytowy, kluczyki, powiązania z ekwipunkiem oraz najlepsze darmowe i płatne skrypty garaży.

Agency Scripts

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

Zrozumienie architektury systemu garaży

System garaży to jedna z najważniejszych funkcji na każdym serwerze roleplay FiveM, stanowiąca podstawowy sposób przechowywania, wyciągania i zarządzania pojazdami przez graczy. W swojej istocie system garaży składa się z trzech połączonych ze sobą warstw: warstwy bazy danych, która zapisuje własność i stan pojazdu, warstwy logiki po stronie serwera, która obsługuje operacje spawnu i despawnu z walidacją, oraz warstwy UI po stronie klienta, która pozwala graczom na interakcję z zaparkowanymi pojazdami. Przed napisaniem jakiegokolwiek kodu musisz podjąć kluczowe decyzje architektoniczne, np. czy garaże są przypisane do lokalizacji, czy globalne, czy gracze mają dostęp do każdego garażu, czy tylko do wybranych, oraz jak chcesz obsługiwać właściwości pojazdu, takie jak modyfikacje, poziom paliwa i stan uszkodzeń. Najlepsze systemy garaży przechowują pełny obiekt właściwości pojazdu, dzięki czemu po wyciągnięciu auta przez gracza wraca ono dokładnie w takim stanie, w jakim zostało pozostawione, łącznie z niestandardowym lakierem, ulepszeniami osiągów, a nawet poziomem zabrudzenia karoserii.

Schemat bazy danych i trwałość pojazdów

Schemat twojej bazy danych stanowi fundament całego systemu garaży. Potrzebujesz tabeli śledzącej własność pojazdu, jego aktualny stan oraz zapisane właściwości. Kolumna state jest kluczowa, ponieważ określa, czy pojazd jest aktualnie zespawnowany w świecie gry, zaparkowany w garażu, czy znajduje się na parkingu policyjnym. Oto praktyczny schemat obejmujący najważniejsze pola:

CREATE TABLE IF NOT EXISTS player_vehicles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    citizenid VARCHAR(50) NOT NULL,
    vehicle VARCHAR(50) NOT NULL,
    hash VARCHAR(50) NOT NULL,
    mods LONGTEXT DEFAULT '{}',
    plate VARCHAR(8) NOT NULL,
    fakeplate VARCHAR(8) DEFAULT NULL,
    garage VARCHAR(50) DEFAULT 'pillboxgarage',
    fuel INT DEFAULT 100,
    engine FLOAT DEFAULT 1000.0,
    body FLOAT DEFAULT 1000.0,
    state INT DEFAULT 1,  -- 0 = out, 1 = garaged, 2 = impounded
    depotprice INT DEFAULT 0,
    drivingdistance INT DEFAULT 0,
    INDEX idx_citizenid (citizenid),
    INDEX idx_plate (plate),
    INDEX idx_state (state)
);

Ten mods kolumna przechowuje zakodowany w formacie JSON obiekt zawierający wszystkie modyfikacje pojazdu zwracane przez funkcje takie jak QBCore.Functions.GetVehicleProperties(vehicle) lub odpowiednik w ESX. Indeksowanie citizenid, plate, oraz state kolumny sprawiają, że wyszukiwanie pozostaje szybkie, nawet gdy baza graczy wzrośnie do tysięcy. Zawsze używaj zapytań parametryzowanych podczas interakcji z tą tabelą, aby zapobiec atakom SQL injection.

Logika spawnu i despawnu po stronie serwera

Po stronie serwera odbywa się cała kluczowa walidacja. Gdy gracz żąda wyciągnięcia pojazdu z garażu, serwer musi sprawdzić, czy gracz faktycznie jest właścicielem tego pojazdu, czy pojazd znajduje się obecnie w stanie zaparkowanym w garażu i czy dostępny jest prawidłowy spawn point. Nigdy nie pozwalaj klientowi bezpośrednio decydować o pozycji spawnu, ponieważ cheaterzy mogliby spawnować pojazdy w dowolnym miejscu na mapie. Zamiast tego zdefiniuj punkty spawnu na serwerze i wybierz najbliższy dostępny. Oto przykład bezpiecznego handlera wyciągania pojazdu po stronie serwera:

RegisterNetEvent('garage:server:takeVehicle', function(vehicleId, garageId)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local citizenid = Player.PlayerData.citizenid
    local result = MySQL.query.await(
        'SELECT * FROM player_vehicles WHERE id = ? AND citizenid = ? AND state = 1',
        {vehicleId, citizenid}
    )

    if not result or not result[1] then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle not found', 'error')
        return
    end

    local vehData = result[1]
    local spawnPoint = GetAvailableSpawnPoint(garageId)

    if not spawnPoint then
        TriggerClientEvent('QBCore:Notify', src, 'No parking spots available', 'error')
        return
    end

    MySQL.update('UPDATE player_vehicles SET state = 0 WHERE id = ?', {vehicleId})
    TriggerClientEvent('garage:client:spawnVehicle', src, vehData, spawnPoint)
end)

W procesie despawnu serwer musi przechwycić aktualne właściwości pojazdu przed usunięciem go ze świata gry. Gwarantuje to zapisanie modyfikacji wprowadzonych od ostatniego zaparkowania. Zawsze aktualizuj poziom paliwa, stan silnika i stan karoserii wraz z JSON modyfikacji, aby wszystko zapisało się poprawnie. Zaimplementuj sprawdzanie odległości po stronie server-side, aby upewnić się, że gracz rzeczywiście znajduje się w pobliżu garażu, zanim zezwolisz na operację schowania.

UI garażu po stronie klienta z NUI

UI garażu to miejsce, w którym gracze wchodzą w interakcję z systemem, a dobrze zaprojektowany interfejs decyduje o różnicy między frustrującym doświadczeniem a płynną rozgrywką. Użyj NUI z HTML, CSS i JavaScript, aby zbudować responsywny panel wyświetlający wszystkie pojazdy przechowywane w danym garażu. Każdy wpis pojazdu powinien na pierwszy rzut oka pokazywać nazwę pojazdu, tablicę rejestracyjną, poziom paliwa oraz ogólny stan techniczny. Dodaj system podglądu, który tymczasowo spawnuje model pojazdu, aby gracze widzieli, co wybierają, co jest szczególnie przydatne, gdy gracz posiada kilka pojazdów tego samego typu. Oto logika client-side do otwierania menu garażu i pobierania danych pojazdu:

RegisterNetEvent('garage:client:openMenu', function(garageId)
    QBCore.Functions.TriggerCallback('garage:server:getVehicles', function(vehicles)
        if not vehicles or #vehicles == 0 then
            QBCore.Functions.Notify('No vehicles stored here', 'info')
            return
        end

        SetNuiFocus(true, true)
        SendNUIMessage({
            action = 'openGarage',
            vehicles = vehicles,
            garageName = Config.Garages[garageId].label
        })
    end, garageId)
end)

RegisterNUICallback('takeVehicle', function(data, cb)
    SetNuiFocus(false, false)
    TriggerServerEvent('garage:server:takeVehicle', data.vehicleId, currentGarage)
    cb('ok')
end)

Po stronie JavaScript renderuj każdy pojazd jako kartę z przyciskami akcji do wyciągania pojazdu lub przenoszenia go do innego garażu. Rozważ dodanie opcji sortowania i filtrowania, aby gracze z dużymi kolekcjami mogli szybko znaleźć potrzebny pojazd. Pasek wyszukiwania filtrujący według numeru rejestracyjnego lub nazwy pojazdu to mały dodatek, który znacznie poprawia wygodę na serwerach, gdzie gracze gromadzą z czasem wiele pojazdów.

Zapisywanie i przywracanie właściwości pojazdu

Prawidłowe zapisywanie i przywracanie właściwości pojazdu to jeden z najtrudniejszych elementów tworzenia systemu garaży. Obiekt properties zawiera dziesiątki pól, w tym kolory, malowania (liveries), neony, przyciemnienie szyb, kolor dymu z opon, dodatki (extras) oraz wszelkie modyfikacje osiągów. Podczas chowania pojazdu pobierz jego właściwości tuż przed usunięciem entity, aby mieć pewność, że otrzymasz najbardziej aktualny stan. Przy ponownym spawnowaniu pojazdu musisz poczekać na pełne załadowanie entity przed nałożeniem właściwości, w przeciwnym razie modyfikacje takie jak niestandardowe koła czy ulepszenia silnika po cichu zawiodą. Zastosuj niewielkie opóźnienie lub odpowiednią pętlę sprawdzającą istnienie entity:

function SpawnAndApplyMods(vehData, spawnPoint)
    local model = GetHashKey(vehData.vehicle)
    RequestModel(model)

    while not HasModelLoaded(model) do
        Wait(10)
    end

    local veh = CreateVehicle(model, spawnPoint.x, spawnPoint.y, spawnPoint.z,
        spawnPoint.w, true, false)

    while not DoesEntityExist(veh) do
        Wait(10)
    end

    local props = json.decode(vehData.mods)
    if props then
        QBCore.Functions.SetVehicleProperties(veh, props)
    end

    SetVehicleFuelLevel(veh, vehData.fuel + 0.0)
    SetVehicleEngineHealth(veh, vehData.engine + 0.0)
    SetVehicleBodyHealth(veh, vehData.body + 0.0)
    SetEntityAsMissionEntity(veh, true, true)
    SetModelAsNoLongerNeeded(model)
    TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1)
end

Zwróć szczególną uwagę na pojazdy addon, ponieważ czasami mają niestandardowe elementy dodatkowe lub indeksy malowań, które zachowują się inaczej niż w przypadku pojazdów vanilla GTA. Dokładnie przetestuj cykl zapisywania i przywracania właściwości na różnych typach pojazdów, aby wcześnie wyłapać skrajne przypadki.

Integracja z systemem odholowywania

System odholowywania współpracuje bezpośrednio z garażem i dodaje warstwę realizmu, której wymagają serwery roleplay. Pojazdy trafiają na parking policyjny z kilku powodów: konfiskata przez policję podczas aresztowania, automatyczne czyszczenie porzuconych pojazdów po restarcie serwera lub interwencja administratora za łamanie regulaminu. Gdy pojazd zostanie odholowany, zaktualizuj jego stan na 2 w bazie danych i opcjonalnie ustaw opłatę parkingową, którą gracz musi uiścić, aby go odebrać. Parking policyjny powinien działać podobnie do garażu, ale z dodatkowym wymogiem opłaty przed wydaniem pojazdu. Utwórz osobną lokalizację parkingu na mapie z własnymi punktami spawnu oraz interfejsem NUI, który wyraźnie wyświetla opłatę za odholowanie.

RegisterNetEvent('police:server:impoundVehicle', function(plate, price)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Verify the player has police job authorization
    if Player.PlayerData.job.name ~= 'police' then return end

    local result = MySQL.update.await(
        'UPDATE player_vehicles SET state = 2, depotprice = ? WHERE plate = ?',
        {price or 500, plate}
    )

    if result > 0 then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded', 'success')
    end
end)

Rozważ wdrożenie wielopoziomowego systemu opłat, w którym opłata za odholowanie na impound rośnie za każdym razem, gdy ten sam pojazd trafia na parking, zniechęcając graczy do traktowania impoundu jako darmowego parkingu. Możesz także dodać mechanikę czasową, w której pojazdy pozostawione na impoundzie przez ponad konfigurowalną liczbę rzeczywistych dni są automatycznie i bezpłatnie zwracane do garażu, co zapobiega scenariuszom trwałej utraty pojazdu, frustrującym graczy.

Blipy garażu i integracja z targetem

Sprawienie, by garaże były łatwe do znalezienia i proste w obsłudze, wymaga odpowiedniego rozmieszczenia blipów i stref interakcji. Dodaj blipy na mapie dla każdej lokalizacji garażu, aby gracze mogli znaleźć je na minimapie, oraz użyj markerów opartych na odległości lub integracji z systemem target jako wyzwalacza interakcji. Systemy target, takie jak ox_target lub qb-target, zapewniają bardziej przejrzyste wrażenia, ponieważ wyświetlają opcje interakcji tylko wtedy, gdy gracz celuje w określony punkt, redukując bałagan na ekranie. Zdefiniuj lokalizacje garaży we wspólnym pliku konfiguracyjnym, z którego może korzystać zarówno client, jak i server, zachowując synchronizację koordynatów, punktów spawnu i ustawień:

Config.Garages = {
    ['pillboxgarage'] = {
        label = 'Pillbox Garage',
        coords = vector3(215.83, -810.18, 30.73),
        spawnPoints = {
            vector4(218.32, -803.28, 30.73, 248.5),
            vector4(222.41, -799.84, 30.73, 248.5),
            vector4(226.52, -796.41, 30.73, 248.5),
        },
        blip = { sprite = 357, color = 3, scale = 0.7 },
        vehicleType = 'car',  -- car, boat, aircraft
    },
}

Obsługuj wiele typów pojazdów, tworząc osobne garaże dla łodzi i statków powietrznych z odpowiednimi punktami spawnu przy wodzie lub na lotniskach. Ten vehicleType filtr zapewnia, że gracze widzą tylko pojazdy lądowe w garażu ulicznym i tylko łodzie w marinie, co zapobiega pomyłkom i problemom ze spawnem. Gdy gracz zbliża się do garażu, sprawdź, czy ma tam zapisane jakiekolwiek pojazdy, zanim wyświetlisz monit o interakcję, aby uniknąć niepotrzebnego otwierania menu dla graczy, którzy nie mają pojazdów w tej lokalizacji.

Wskazówki dotyczące optymalizacji wydajności

Systemy garaży mogą stać się wąskim gardłem wydajności, jeśli nie zostaną zaimplementowane ostrożnie, zwłaszcza na serwerach z setkami graczy online jednocześnie, z których każdy posiada wiele pojazdów. Cache'uj listy pojazdów po stronie serwera zamiast odpytywać bazę danych za każdym razem, gdy gracz otwiera menu garażu, i unieważniaj cache tylko wtedy, gdy stan pojazdu ulegnie zmianie. Po stronie klienta unikaj pozostawiania otwartych ramek NUI, gdy nie są potrzebne, ponieważ nawet ukryte ramki NUI zużywają zasoby, jeśli działają w nich timery lub animacje JavaScript. Podczas spawnowania pojazdów upewnij się, że prawidłowo czyścisz encje, oznaczając je jako niepotrzebne po tym, jak gracz je odstawi, oraz wdróż awaryjną procedurę czyszczenia uruchamianą okresowo, aby wyłapywać osierocone encje pojazdów, które nie zostały poprawnie usunięte z powodu crashy lub rozłączeń. Używaj funkcji natywnych, takich jak GetGamePool('CVehicle') oszczędnie i cachuj wyniki, gdy musisz sprawdzić istniejące pojazdy graczy w świecie gry. Na koniec rozważ wdrożenie maksymalnego limitu pojazdów na garaż, aby ograniczyć liczbę zapytań do bazy danych i uniemożliwić pojedynczemu graczowi przechowywanie setek pojazdów, co mogłoby spowolnić operacje ich pobierania.

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.