Wróć do bloga
Tutorial8 min czytania

System holowników w FiveM: oskryptuj profesjonalną pracę holownika

Prowadź pracę holownika w FiveM. Wezwania od NPC, logika odholowywania, animacje lawety i najlepsze skrypty holownicze do integracji z mechanikami i policją.

Agency Scripts

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

Przegląd systemu holownika

System holownika i parkingu depozytowego (impound) tworzy cały ekosystem pracy na twoim serwerze FiveM, dając graczom cywilną ścieżkę kariery, która naturalnie współgra ze służbami porządkowymi, mechanikami i innymi kierowcami. System obejmuje wiele powiązanych ze sobą komponentów: warstwę zarządzania pracą obsługującą rozpoczynanie i kończenie służby, system dispatch kierujący zgłoszenia holowania do dostępnych kierowców, mechanikę mocowania pojazdów symulującą realistyczne holowanie, parking depozytowy przechowujący skonfiskowane i porzucone pojazdy oraz strukturę opłat regulującą koszt odbioru odholowanego pojazdu. Przy dobrym projekcie praca kierowcy holownika staje się jedną z najbardziej angażujących społecznie ról na serwerze, ponieważ każde wezwanie wiąże się z bezpośrednią interakcją z innym graczem potrzebującym pomocy z uszkodzonym lub nieprawidłowo zaparkowanym pojazdem.

Konfiguracja prac i system służby

Zacznij od zdefiniowania pracy kierowcy lawety w systemie prac swojego frameworka. Gracze powinni mieć możliwość rozpoczęcia służby w bazie holowników, otrzymania firmowej lawety i przyjmowania zgłoszeń dispatch. System służby powinien śledzić aktywnych kierowców lawet, aby system dispatch wiedział, kto jest dostępny. Gdy gracz wchodzi na służbę, zrespawnuj pojazd typu flatbed ze zdefiniowanej wcześniej listy modeli holowników i przypisz go temu graczowi. Gdy schodzi ze służby, pojazd firmowy powinien zostać zwrócony lub usunięty. Zapisuj status służby gracza po stronie serwera, aby poprawnie się utrzymywał i mógł być odpytywany przez system dispatch:

Config.TowJob = {
    jobName = 'tow',
    clockInLocation = vector3(409.09, -1622.57, 29.29),
    vehicleSpawn = vector4(405.87, -1631.14, 29.29, 228.35),
    towVehicles = {
        {model = 'flatbed', label = 'Flatbed Truck', rank = 0},
        {model = 'towtruck', label = 'Tow Truck (Hook)', rank = 1},
        {model = 'towtruck2', label = 'Heavy Tow Truck', rank = 2},
    },
    impoundLot = vector3(409.59, -1637.87, 29.29),
    impoundReturn = vector4(403.12, -1640.23, 29.29, 138.92),
    payPerTow = {min = 350, max = 750},
    payPerImpound = 500,
}

RegisterNetEvent('tow:server:clockIn', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    if Player.PlayerData.job.name ~= Config.TowJob.jobName then
        TriggerClientEvent('QBCore:Notify', src, 'You are not a tow driver', 'error')
        return
    end

    Player.Functions.SetJobDuty(true)
    TowDrivers[src] = {
        onDuty = true,
        currentCall = nil,
        vehicleNet = nil,
        towedVehicle = nil,
    }

    TriggerClientEvent('QBCore:Notify', src, 'You are now on duty', 'success')
    TriggerClientEvent('tow:client:spawnVehicle', src)
end)

Mechanika doczepiania pojazdów

Głównym wyzwaniem technicznym systemu holowania jest połączenie jednego pojazdu z drugim w sposób, który wygląda realistycznie i zachowuje się poprawnie podczas jazdy. FiveM udostępnia funkcję AttachEntityToEntity native do tego celu, ale prawidłowe ustawienie pozycji offsetu wymaga starannej kalibracji dla każdego modelu lawety. Wariant z platformą sprawdza się najlepiej dla większości pojazdów: holowany samochód jest umieszczany na platformie z odpowiednimi przesunięciami, aby naturalnie na niej spoczywał. W przypadku holowników z hakiem przednie koła holowanego pojazdu są unoszone, podczas gdy tylne pozostają na ziemi. Każdy styl mocowania wymaga innych wektorów offsetu i wartości rotacji. Przetestuj te wartości w grze, wprowadzając drobne korekty, aż pojazd będzie osadzony prawidłowo i nie będzie przenikał przez ciężarówkę:

function AttachVehicleToFlatbed(towTruck, targetVehicle)
    local towModel = GetEntityModel(towTruck)

    -- Offsets calibrated for the flatbed model
    local offsets = {
        ['flatbed'] = {x = 0.0, y = -0.5, z = 1.1, rx = 0.0, ry = 0.0, rz = 0.0},
    }

    local offset = offsets['flatbed']
    if not offset then return false end

    -- Freeze target vehicle physics
    SetEntityCollision(targetVehicle, false, false)
    FreezeEntityPosition(targetVehicle, true)

    -- Attach to flatbed
    AttachEntityToEntity(
        targetVehicle, towTruck,
        0, -- bone index
        offset.x, offset.y, offset.z,
        offset.rx, offset.ry, offset.rz,
        false, false, false, false, 0, true
    )

    -- Store attachment state
    TriggerServerEvent('tow:server:vehicleAttached', VehToNet(towTruck), VehToNet(targetVehicle))
    return true
end

function DetachVehicleFromFlatbed(towTruck, targetVehicle)
    DetachEntity(targetVehicle, true, true)
    SetEntityCollision(targetVehicle, true, true)
    FreezeEntityPosition(targetVehicle, false)

    -- Place vehicle on ground behind the truck
    local truckCoords = GetEntityCoords(towTruck)
    local truckHeading = GetEntityHeading(towTruck)
    local behind = GetOffsetFromEntityInWorldCoords(towTruck, 0.0, -12.0, 0.0)

    SetEntityCoords(targetVehicle, behind.x, behind.y, behind.z)
    SetEntityHeading(targetVehicle, truckHeading)
    SetVehicleOnGroundProperly(targetVehicle)

    TriggerServerEvent('tow:server:vehicleDetached', VehToNet(towTruck))
end

Ważnym szczegółem jest wyłączenie kolizji w doczepionym pojeździe, aby zapobiec błędom fizyki. Gdy dwa pojazdy mają aktywną kolizję i są ze sobą połączone, silnik gry może wywołać gwałtowne drżenie lub wystrzelić pojazdy w powietrze. Zamrożenie pozycji docelowego pojazdu i wyłączenie jego kolizji na czas połączenia całkowicie eliminuje te problemy.

System dispatch i zleceń holowania

System dyspozytorski łączy graczy potrzebujących holowania z dostępnymi kierowcami lawet. Gracze mogą wezwać holowanie przez telefon, u mechanika NPC lub przez budki alarmowe przy drodze rozmieszczone na mapie. Gdy pojawia się zgłoszenie, serwer sprawdza dostępnych kierowców lawet i przekazuje wezwanie najbliższemu z nich. Jeśli żaden kierowca nie jest na służbie, udostępnij zapasową usługę holowania przez NPC, która pobiera wyższą opłatę i po pewnym czasie teleportuje pojazd na parking policyjny. Powiadomienie dispatch powinno zawierać lokalizację zgłaszającego, model pojazdu oraz powód holowania, taki jak awaria, wypadek lub nieprawidłowe parkowanie. Daj kierowcy lawety możliwość przyjęcia lub odrzucenia zgłoszenia, przy czym odrzucone wezwania są przekazywane kolejnemu najbliższemu kierowcy:

RegisterNetEvent('tow:server:requestTow', function(reason)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local playerCoords = GetEntityCoords(GetPlayerPed(src))
    local vehicle = GetVehiclePedIsIn(GetPlayerPed(src), true)
    local vehicleModel = vehicle ~= 0 and GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)) or 'Unknown'

    -- Find nearest available tow driver
    local nearestDriver = nil
    local nearestDist = math.huge

    for driverId, data in pairs(TowDrivers) do
        if data.onDuty and not data.currentCall then
            local driverCoords = GetEntityCoords(GetPlayerPed(driverId))
            local dist = #(playerCoords - driverCoords)
            if dist < nearestDist then
                nearestDist = dist
                nearestDriver = driverId
            end
        end
    end

    if nearestDriver then
        TowDrivers[nearestDriver].currentCall = {
            requesterId = src,
            coords = playerCoords,
            vehicle = vehicleModel,
            reason = reason,
            timestamp = os.time(),
        }

        TriggerClientEvent('tow:client:incomingCall', nearestDriver, {
            coords = playerCoords,
            vehicle = vehicleModel,
            reason = reason,
            playerName = Player.PlayerData.charinfo.firstname,
        })

        TriggerClientEvent('QBCore:Notify', src, 'Tow truck dispatched. ETA based on driver location.', 'success')
    else
        TriggerClientEvent('QBCore:Notify', src, 'No tow drivers available. NPC service will arrive shortly.', 'info')
        StartNPCTowService(src, playerCoords, vehicle)
    end
end)

Zarządzanie parkingiem depozytowym

Parking policyjny to miejsce, w którym odholowane i skonfiskowane pojazdy są przechowywane do momentu ich odebrania przez właścicieli. Zaprojektuj bazę danych odholowań, aby śledzić, które pojazdy są aktualnie odholowane, kto je odholował, kiedy to nastąpiło oraz jaka opłata jest wymagana do ich wydania. Policja powinna mieć możliwość odholowywania pojazdów bezpośrednio podczas kontroli drogowych lub po pościgach, przy czym takie odholowania mogą wiązać się z wyższymi opłatami lub dłuższym obowiązkowym czasem zatrzymania. Interfejs parkingu policyjnego powinien wyświetlać listę odholowanych pojazdów gracza wraz ze szczegółami, takimi jak model pojazdu, numer rejestracyjny, data odholowania, powód i opłata za odbiór. Gracze uiszczają opłatę w okienku parkingu policyjnego, a ich pojazd pojawia się w wyznaczonym punkcie odbioru w pobliżu:

CREATE TABLE IF NOT EXISTS impounded_vehicles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    plate VARCHAR(10) NOT NULL,
    citizenid VARCHAR(50) NOT NULL,
    vehicle_data LONGTEXT NOT NULL,
    impound_reason ENUM('tow', 'police', 'abandoned', 'parking') DEFAULT 'tow',
    impound_fee INT DEFAULT 250,
    impounded_by VARCHAR(50) DEFAULT NULL,
    impounded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    hold_until TIMESTAMP DEFAULT NULL,
    released_at TIMESTAMP DEFAULT NULL,
    INDEX idx_plate (plate),
    INDEX idx_citizen (citizenid),
    INDEX idx_released (released_at)
);

-- Server-side: Impound a vehicle
RegisterNetEvent('tow:server:impoundVehicle', function(vehicleNet, reason)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local vehicle = NetworkGetEntityFromNetworkId(vehicleNet)
    if not DoesEntityExist(vehicle) then return end

    local plate = GetVehicleNumberPlateText(vehicle)
    local vehicleData = QBCore.Functions.GetVehicleProperties(vehicle)
    local fee = Config.ImpoundFees[reason] or 250

    MySQL.insert('INSERT INTO impounded_vehicles (plate, citizenid, vehicle_data, impound_reason, impound_fee, impounded_by) VALUES (?, ?, ?, ?, ?, ?)', {
        plate:trim(),
        GetVehicleOwner(plate),
        json.encode(vehicleData),
        reason,
        fee,
        Player.PlayerData.citizenid,
    })

    DeleteEntity(vehicle)
    local pay = math.random(Config.TowJob.payPerImpound - 100, Config.TowJob.payPerImpound + 100)
    Player.Functions.AddMoney('bank', pay, 'tow-impound')
    TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded. Earned $' .. pay, 'success')
end)

Zautomatyzowana kontrola parkowania

Dodaj zautomatyzowany system wykrywający pojazdy zaparkowane w strefach z zakazem parkowania i oznaczający je do odholowania po konfigurowalnym czasie karencji. Zdefiniuj strefy ograniczonego parkowania wokół hydrantów, przystanków autobusowych, komisariatów policji i podjazdów ratunkowych szpitali. Gdy gracz zaparkuje w strefie zastrzeżonej i odejdzie, uruchom licznik. Jeśli pojazd pozostanie bez opieki po upływie czasu karencji, wygeneruj zlecenie holowania i powiadom dostępnych kierowców lawet. Zapewnia to naturalną pracę kierowcom pomocy drogowej bez konieczności ręcznego wzywania służb przez graczy. System powinien oznaczać tylko pojazdy należące do graczy, ponieważ pojazdy NPC są zarządzane przez silnik gry i znikają w naturalny sposób. Dołącz powiadomienie dla właściciela pojazdu, że jego samochód został oznaczony do odholowania, dając mu szansę na powrót i przestawienie go przed przyjazdem lawety.

Przychody i progresja

Zorganizuj pracę holownika w oparciu o system progresji, który nagradza regularną pracę. Nowi kierowcy holowników zaczynają od rangi zero z dostępem do podstawowej lawety i zarabiają stawkę bazową za każde zrealizowane holowanie. W miarę gromadzenia ukończonych zleceń awansują i odblokowują cięższe pojazdy holownicze zdolne do obsługi większych pojazdów, takich jak ciężarówki i autobusy, dostęp do lepiej płatnych zleceń z policyjnego parkingu depozytowego oraz osobistą lawetę, którą mogą personalizować. Śledź statystyki, takie jak łączna liczba ukończonych holowań, przejechany dystans podczas holowania oraz średni czas reakcji na wezwania dyspozytora. Wyświetlaj te statystyki w panelu pracy dostępnym u NPC lub w aplikacji na telefonie. Rozważ wdrożenie systemu reputacji, w którym kierowcy holowników konsekwentnie szybko reagujący i kończący zlecenia bez uszkodzeń pojazdów otrzymują mnożniki bonusowe do wypłaty, podczas gdy kierowcy ignorujący wezwania lub uszkadzający pojazdy podczas holowania ponoszą kary finansowe. Tworzy to naturalną motywację do dbania o jakość, która przynosi korzyści całej społeczności serwera, zapewniając niezawodne i profesjonalne usługi holownicze.

Obsługa przypadków brzegowych i kwestii sieciowych

Systemy holowania mają kilka problematycznych przypadków brzegowych, które wymagają ostrożnej obsługi. Gdy kierowca holownika rozłączy się podczas holowania pojazdu, serwer powinien wykryć rozłączenie, a następnie bezpiecznie odczepić i postawić holowany pojazd na ziemi, aby nie unosił się w powietrzu ani nie wpadł pod mapę. Jeśli właściciel pojazdu zaloguje się, gdy jego samochód jest podczepiony do holownika, powinien zobaczyć swój pojazd oznaczony jako holowany na liście pojazdów wraz z informacjami o kierowcy holownika. Ostrożnie zarządzaj własnością entity pojazdu, ponieważ funkcja native do przyczepiania działa niezawodnie tylko wtedy, gdy klient mocujący jest właścicielem obu entities. Użyj NetworkRequestControlOfEntity aby upewnić się, że klient kierowcy holownika kontroluje oba pojazdy przed próbą zaczepienia. W celu synchronizacji w trybie multiplayer rozgłoś stan zaczepienia do wszystkich pobliskich graczy, aby widzieli holowany pojazd we właściwej pozycji na swoich ekranach. State bags świetnie się do tego sprawdzają, ponieważ automatycznie replikują się do wszystkich klientów w zasięgu obiektu.

Gotowy, aby zacząć?

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