Назад к блогу
Tutorial9 мин чтения

Система такси для FiveM: скрипты для NPC и игроков-водителей такси

Добавьте работающую систему такси в FiveM. NPC такси, водители-игроки, логика тарифа, счётчики и лучшие скрипты такси для оживлённых ролевых городов.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Архитектура системы работы такси

Система работы таксиста , ключевая функция для любого ролевого сервера, желающего предоставить игрокам доступную начальную работу с настоящим взаимодействием между игроками. В отличие от многих других профессий, которые могут казаться однообразными и утомительными, хорошо продуманная система такси создаёт органичные RP-ситуации, когда водители подбирают пассажиров, договариваются о цене и ориентируются по городу под давлением времени. Архитектура делится на два режима: транспортировка игрок-игрок, где реальные пассажиры заказывают поездки, и режим миссий с NPC, где система генерирует точки посадки и высадки для одиночной игры в непиковые часы. Оба режима используют один и тот же движок расчёта стоимости, интерфейс таксометра и систему управления транспортом. Ключевое решение , использовать централизованную диспетчерскую систему с общей очередью заявок, видимой всем дежурным водителям, или систему на основе близости, где приоритет получает ближайший водитель. Большинство успешных реализаций используют гибридный подход с диспетчерской доской, показывающей все заявки, но выделяющей ближайшие, давая водителям свободу выбора и обеспечивая быструю посадку пассажиров.

Реализация таксометра

Таксометр , сердце системы тарификации, он должен выглядеть аутентично и оставаться прозрачным для водителя и пассажира. Правильный таксометр отслеживает две составляющие: базовый тариф, взимаемый в начале каждой поездки, и тариф за пройденное расстояние, который увеличивается по мере движения автомобиля. Некоторые серверы также добавляют тариф за время ожидания, взимающий меньшую плату за секунду, когда такси стоит на месте, имитируя реальную практику оплаты времени в пробках. Таксометр должен быть виден и водителю, и пассажирам через постоянный элемент NUI, расположенный в углу экрана. Вот реализация таксометра на стороне клиента, который рассчитывает стоимость поездки по пройденному расстоянию:

local meterActive = false
local currentFare = 0.0
local lastPosition = nil
local baseFare = 50
local ratePerMile = 15
local waitRatePerSecond = 0.5
local totalDistance = 0.0

function StartMeter()
    meterActive = true
    currentFare = baseFare
    totalDistance = 0.0
    lastPosition = GetEntityCoords(PlayerPedId())

    SendNUIMessage({action = 'showMeter', fare = currentFare, distance = 0.0})

    CreateThread(function()
        while meterActive do
            Wait(500)
            local currentPos = GetEntityCoords(PlayerPedId())
            local speed = GetEntitySpeed(PlayerPedId())

            if speed > 1.0 then
                local dist = #(currentPos - lastPosition)
                totalDistance = totalDistance + dist
                local miles = totalDistance / 1609.34
                currentFare = baseFare + (miles * ratePerMile)
            else
                currentFare = currentFare + (waitRatePerSecond * 0.5)
            end

            lastPosition = currentPos

            SendNUIMessage({
                action = 'updateMeter',
                fare = math.floor(currentFare),
                distance = math.floor(totalDistance),
                speed = math.floor(speed * 2.236936)
            })
        end
    end)
end

function StopMeter()
    meterActive = false
    local finalFare = math.floor(currentFare)
    SendNUIMessage({action = 'stopMeter', fare = finalFare})
    return finalFare
end

Компонент NUI для счётчика должен быть компактным, но читаемым, показывая текущий тариф крупно вместе с пройденным расстоянием и применяемым тарифом. Используйте моноширинный шрифт для отображения тарифа, чтобы имитировать вид настоящего таксометра, и добавьте тонкую анимацию тика при каждом увеличении тарифа, чтобы дать визуальную обратную связь о работе счётчика. Рассмотрите возможность добавления функции оценки стоимости, которая рассчитывает приблизительную цену на основе прямой дистанции до пункта назначения до начала поездки, чтобы пассажиры могли решить, хотят ли они продолжать.

Система миссий с NPC для посадки

Миссии с NPC заняты таксистов, когда на сервере недостаточно реальных пассажиров. Система генерирует случайные точки посадки и высадки из тщательно подобранного списка интересных мест по карте, создаёт NPC в точке посадки и ведёт водителя через полный цикл поездки. Качество миссии с NPC сильно зависит от данных о локациях: использование только случайных координат приводит к неестественным посадкам в глуши, поэтому поддержание вручную подобранного списка реалистичных мест, таких как больницы, рестораны, отели и торговые центры, делает миссии правдоподобными. Вот серверная логика для генерации и отправки миссий с NPC:

local MissionLocations = {
    {coords = vector3(-240.49, -987.78, 29.29), label = 'Pillbox Medical Center'},
    {coords = vector3(228.28, -790.70, 30.58), label = 'Legion Square'},
    {coords = vector3(-1044.57, -2739.76, 13.85), label = 'Los Santos Airport'},
    {coords = vector3(126.19, -1282.72, 29.27), label = 'Strawberry Avenue'},
    {coords = vector3(-556.18, -196.17, 38.22), label = 'Rockford Hills'},
    {coords = vector3(1159.56, -314.45, 69.21), label = 'Vinewood Hills'},
    {coords = vector3(-1222.08, -906.82, 12.32), label = 'Del Perro Pier'},
    {coords = vector3(373.58, 325.71, 103.57), label = 'Downtown Vinewood'},
}

RegisterNetEvent('taxi:server:requestNPCMission', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player or Player.PlayerData.job.name ~= 'taxi' then return end

    local pickup = MissionLocations[math.random(#MissionLocations)]
    local dropoff = MissionLocations[math.random(#MissionLocations)]

    while pickup.label == dropoff.label do
        dropoff = MissionLocations[math.random(#MissionLocations)]
    end

    local estimatedDistance = #(pickup.coords - dropoff.coords)
    local basePay = 50
    local distancePay = math.floor(estimatedDistance / 1609.34 * 15)
    local totalPay = basePay + distancePay

    TriggerClientEvent('taxi:client:startNPCMission', src, {
        pickup = pickup,
        dropoff = dropoff,
        estimatedPay = totalPay,
        npcModel = Config.NPCModels[math.random(#Config.NPCModels)],
        missionId = 'TAXI-' .. os.time() .. '-' .. math.random(1000, 9999)
    })
end)

На клиентской стороне создайте NPC в точке выдачи с анимацией ожидания, добавьте блип для навигации водителя и воспроизведите анимацию посадки при прибытии водителя. Во время поездки NPC должен демонстрировать естественное поведение: осматриваться и время от времени комментировать в чат-баблах. По прибытии на место NPC выходит, а оплата автоматически производится через серверное событие. Добавьте разнообразия, периодически генерируя специальные миссии с повышенной оплатой, например, трансферы в аэропорт или VIP-клиентов, требующих аккуратного вождения без столкновений.

Расчёт стоимости проезда и повышенные тарифы

Помимо базового расчёта на основе расстояния, продвинутая система тарификации учитывает время суток, дорожные условия и тип транспортного средства. Реализуйте повышенные тарифы в часы пик на сервере, когда онлайн высокий и спрос на такси превышает предложение. Множитель повышения тарифа должен быть прозрачно сообщён пассажирам до подтверждения поездки, чтобы избежать сюрпризов в конце. Вы можете динамически рассчитывать спрос, отслеживая соотношение ожидающих запросов на поездку к доступным водителям. Вот движок расчёта тарифа, который учитывает эти переменные:

function CalculateFare(pickupCoords, dropoffCoords, vehicleClass)
    local distance = #(pickupCoords - dropoffCoords)
    local miles = distance / 1609.34

    local rates = Config.FareRates
    local baseFare = rates.base
    local perMile = rates.perMile

    -- Vehicle class multiplier
    local classMultiplier = {
        [0] = 0.8,   -- Compacts (budget)
        [1] = 1.0,   -- Sedans (standard)
        [2] = 1.0,   -- SUVs
        [3] = 1.2,   -- Coupes (comfort)
        [18] = 1.5,  -- Super (luxury)
    }
    local vehMultiplier = classMultiplier[vehicleClass] or 1.0

    -- Time-of-day multiplier (night premium)
    local hour = GetClockHours()
    local timeMultiplier = 1.0
    if hour >= 22 or hour <= 5 then
        timeMultiplier = 1.3  -- 30% night surcharge
    end

    -- Surge pricing based on demand
    local surgeMultiplier = GetCurrentSurgeMultiplier()

    local totalFare = (baseFare + (miles * perMile)) * vehMultiplier * timeMultiplier * surgeMultiplier

    return {
        total = math.floor(totalFare),
        breakdown = {
            base = baseFare,
            distance = math.floor(miles * perMile),
            vehicleClass = vehMultiplier,
            nightSurcharge = timeMultiplier > 1.0,
            surgeActive = surgeMultiplier > 1.0,
            surgeRate = surgeMultiplier
        }
    }
end

Отображайте разбивку стоимости проезда пассажиру при запросе поездки, чтобы он понимал, за что платит. Если активировано повышение цен (surge pricing), четко показывайте множитель другим цветом в интерфейсе. Такая прозрачность укрепляет доверие между водителями и пассажирами и снижает споры по суммам оплаты. Позвольте пассажирам выбирать предпочитаемый класс автомобиля при бронировании: бюджетные варианты , компактные машины, а люксовые , премиальные седаны или внедорожники с повышенными тарифами.

Отображение маршрутов и GPS-навигация

Визуальное руководство по маршруту делает опыт таксиста более плавным как для новичков, так и для опытных водителей. FiveM предоставляет SetNewWaypoint native для базовой маршрутизации GPS, но более отточенный подход рисует маршрут прямо на миникарте и опционально отображает 3D путь в мире. Используйте SetBlipRoute функция на метках назначения для включения встроенного GPS-маршрута и улучшения его пользовательскими метками на ключевых путевых точках. Для удобства пассажира отображайте предполагаемое время прибытия и оставшееся расстояние в UI счетчика, обновляя в реальном времени по мере движения транспортного средства. Вот как настроить систему отображения маршрута с отслеживанием путевых точек:

local routeBlip = nil
local checkpointBlips = {}

function SetTaxiRoute(destination, checkpoints)
    ClearTaxiRoute()

    routeBlip = AddBlipForCoord(destination.x, destination.y, destination.z)
    SetBlipSprite(routeBlip, 198)
    SetBlipColour(routeBlip, 5)
    SetBlipScale(routeBlip, 1.0)
    SetBlipRoute(routeBlip, true)
    SetBlipRouteColour(routeBlip, 5)
    BeginTextCommandSetBlipName('STRING')
    AddTextComponentSubstringPlayerName('Destination')
    EndTextCommandSetBlipName(routeBlip)

    if checkpoints then
        for i, cp in ipairs(checkpoints) do
            local cpBlip = AddBlipForCoord(cp.x, cp.y, cp.z)
            SetBlipSprite(cpBlip, 1)
            SetBlipColour(cpBlip, 3)
            SetBlipScale(cpBlip, 0.6)
            table.insert(checkpointBlips, cpBlip)
        end
    end
end

function ClearTaxiRoute()
    if routeBlip then
        SetBlipRoute(routeBlip, false)
        RemoveBlip(routeBlip)
        routeBlip = nil
    end
    for _, blip in ipairs(checkpointBlips) do
        RemoveBlip(blip)
    end
    checkpointBlips = {}
end

function UpdateETADisplay(destination)
    CreateThread(function()
        while routeBlip do
            Wait(2000)
            local pos = GetEntityCoords(PlayerPedId())
            local remaining = #(pos - destination)
            local speed = GetEntitySpeed(PlayerPedId())

            local etaSeconds = speed > 2.0 and (remaining / speed) or 0
            local etaMinutes = math.floor(etaSeconds / 60)

            SendNUIMessage({
                action = 'updateETA',
                distance = math.floor(remaining),
                eta = etaMinutes > 0 and (etaMinutes .. ' min') or 'Arriving'
            })
        end
    end)
end

Для дополнительного погружения реализуйте систему рейтинга, где пассажиры оценивают поездку по таким параметрам, как время в пути относительно оценки, количество столкновений во время поездки и соблюдение водителем правил дорожного движения. Отображайте средний рейтинг водителя в его профиле такси, чтобы пассажиры видели его перед принятием заказа, создавая стимул для водителей предоставлять качественный сервис.

Система чаевых и рейтинги водителей

Система чаевых добавляет социальный элемент, поощряющий водителей предоставлять отличный сервис. После оплаты поездки и выхода пассажира отображайте запрос на чаевые с предустановленными вариантами: 10%, 15%, 20% или произвольная сумма. Чаевые идут напрямую водителю поверх базового тарифа и комиссии таксопарка. Отслеживайте суммарные чаевые каждого водителя в базе данных для создания таблиц лидеров и разблокировки бонусов для лучших водителей. На сервере обрабатывайте чаевые через ту же систему транзакций, что и тарифы, чтобы обеспечить согласованность и избежать дублирования. Водители с высоким рейтингом и средним размером чаевых могут получить бонусы: доступ к премиум-транспорту, сниженные комиссии или приоритет в очереди диспетчера. Совместите систему чаевых с системой отзывов, где пассажиры оставляют краткий текстовый отзыв, видимый водителю, создавая обратную связь, улучшающую качество такси на сервере.

Панель диспетчера и заявки на поездки

Диспетчерская доска , центральный узел, где таксисты находят следующую работу. Создайте её как NUI-панель, доступную из штаба таксопарка или через горячую клавишу на дежурстве. Доска должна отображать все ожидающие заявки, отсортированные по близости, с каждой записью, показывающей имя пассажира, место посадки, пункт назначения, предполагаемый тариф и расстояние. Когда водитель принимает заказ, удаляйте его с доски для всех остальных водителей и запускайте таймер обратного отсчёта; если водитель не прибывает к месту посадки в отведённое время, заказ возвращается на доску, а водителю начисляется штраф за надёжность. Для пассажиров создайте простое приложение телефона или команду, позволяющую запросить поездку, указав текущее местоположение и пункт назначения. Запрос попадает в очередь диспетчеризации, и пассажир получает обновления по мере принятия и приближения водителя. Реализуйте систему отмены с небольшой платой за поздние отмены, чтобы предотвратить повторные запросы и отмены, которые тратят время водителей и создают неприятный опыт.

Управление транспортом и система автопарка

Полная система такси требует правильного управления транспортом, чтобы водители использовали подходящие машины, а компания поддерживала свой автопарк. Создайте систему выдачи автомобилей на таксопарке, где водители выбирают из доступных машин компании. Каждая машина должна иметь учёт пробега, уровень топлива и состояние, а водители отвечают за возврат автомобилей в приемлемом состоянии или сталкиваются с вычетами на ремонт из заработка. В парке должны быть разные классы машин: экономичные седаны для стандартных поездок, комфортные внедорожники для премиум-сервиса и люксовые автомобили для VIP-заказов. Отслеживайте статистику использования каждого автомобиля, включая количество выполненных заказов, выручку и затраты на обслуживание. Реализуйте систему расхода топлива, требующую заправки на АЗС во время смены, добавляя реалистичные операционные расходы, которые опытные водители научатся эффективно контролировать. В конце смены водитель возвращает машину на базу, рассчитывается общий заработок за вычетом комиссии компании и затрат на повреждения, а чистая сумма зачисляется на банковский счёт. Этот полный цикл создаёт реалистичный опыт работы, отражающий реальные операции таксомоторных компаний.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.