Volver al blog
Tutorial12 min de lectura

Sistema de taxis para FiveM: programación de conductores NPC y jugadores

Añade un sistema de taxis funcional a FiveM. Taxis NPC, conductores jugadores, cálculo de tarifas y taxímetros para dar vida a las ciudades de rol.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

Arquitectura del sistema de taxis

Un sistema de taxis es una función fundamental para cualquier servidor de roleplay que quiera ofrecer a los jugadores un trabajo accesible para principiantes con interacciones reales entre jugadores. A diferencia de muchos trabajos que pueden resultar repetitivos y tediosos, un sistema de taxis bien diseñado genera encuentros de roleplay espontáneos mientras los conductores recogen pasajeros, negocian tarifas y recorren la ciudad contra reloj. La arquitectura se divide en dos modos distintos: transporte entre jugadores, donde pasajeros reales solicitan viajes, y misiones con NPC, donde el sistema genera puntos de recogida y destino para jugar en solitario durante las horas de menor actividad. Ambos modos comparten el mismo motor de cálculo de tarifas, la interfaz del taxímetro y el sistema de gestión de vehículos. La decisión de diseño clave es elegir entre un sistema de despacho centralizado, donde las solicitudes de viaje se añaden a una cola compartida visible para todos los conductores de turno, o un sistema basado en la proximidad, donde el conductor más cercano tiene prioridad. La mayoría de las implementaciones exitosas usan un enfoque híbrido con un panel de despacho que muestra todas las solicitudes pendientes y resalta las que están cerca, lo que permite a los conductores elegir sus trabajos y garantiza que los pasajeros sean recogidos rápidamente.

Implementación del taxímetro

El taxímetro es el núcleo del sistema de tarifas y debe resultar auténtico, a la vez que transparente tanto para el conductor como para el pasajero. Un taxímetro adecuado registra dos componentes: una tarifa base que se cobra al inicio de cada viaje y una tarifa por distancia que aumenta a medida que avanza el vehículo. Algunos servidores también añaden una tarifa por tiempo de espera, que cobra una cantidad menor por segundo cuando el taxi está parado y simula la práctica real de cobrar por el tiempo que se pasa en el tráfico. El taxímetro debe estar visible tanto para el conductor como para los pasajeros del vehículo mediante un elemento NUI persistente ubicado en una esquina de la pantalla. Aquí tienes una implementación del taxímetro del lado del cliente que calcula la tarifa según la distancia recorrida:

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

El componente NUI del taxímetro debe ser compacto, pero legible, y mostrar claramente la tarifa actual junto con la distancia recorrida y la tarifa vigente. Usa una fuente monoespaciada para mostrar la tarifa e imitar el aspecto de un taxímetro real, y añade una animación sutil de tic cada vez que aumente la tarifa para indicar visualmente que el taxímetro está funcionando. Considera añadir una función para estimar la tarifa que calcule el coste aproximado según la distancia en línea recta hasta el destino antes de iniciar el viaje, para que los pasajeros puedan decidir si quieren continuar.

Sistema de misiones de recogida de NPC

Las misiones con NPC mantienen ocupados a los taxistas cuando no hay suficientes pasajeros reales en el servidor. El sistema genera puntos aleatorios de recogida y destino a partir de una lista seleccionada de lugares de interés repartidos por el mapa, crea un NPC en el punto de recogida y guía al conductor durante todo el ciclo del viaje. La calidad de la experiencia de las misiones con NPC depende en gran medida de los datos de ubicación: usar solo coordenadas aleatorias crea recogidas poco naturales en medio de la nada, por lo que mantener una lista seleccionada manualmente de ubicaciones realistas, como hospitales, restaurantes, hoteles y centros comerciales, hace que las misiones parezcan auténticas. Aquí tienes la lógica del lado del servidor para generar y asignar misiones con 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)

Del lado del cliente, genera el NPC en el punto de recogida con una animación de espera, añade un blip para guiar al conductor y reproduce una animación de entrada al vehículo cuando llegue. Durante el viaje, el NPC debe mostrar comportamientos naturales, como mirar alrededor y comentar de vez en cuando cómo conduce el taxista mediante burbujas de chat. Cuando el vehículo llega al destino, el NPC se baja y la tarifa se paga automáticamente mediante un evento del servidor. Añade variedad generando ocasionalmente misiones especiales con pagos más altos, como traslados al aeropuerto o clientes VIP que exigen una conducción cuidadosa y sin colisiones.

Cálculo de tarifas y precios dinámicos

Además del cálculo básico basado en la distancia, un sistema de tarifas sofisticado tiene en cuenta la hora del día, las condiciones del tráfico y el tipo de vehículo. Implementa precios dinámicos en el servidor durante las horas punta, cuando hay muchos jugadores en línea y la demanda de taxis supera la oferta. El multiplicador de demanda debe comunicarse de forma transparente a los pasajeros antes de que confirmen el viaje para evitar sorpresas al final. Puedes calcular la demanda de forma dinámica mediante el seguimiento de la proporción entre solicitudes de viaje pendientes y conductores disponibles. Aquí tienes un motor de cálculo de tarifas que incorpora estas variables:

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

Muestra al pasajero el desglose de la tarifa cuando solicita un viaje para que entienda por qué está pagando. Si los precios dinámicos están activos, muestra claramente el multiplicador con un indicador de otro color en la interfaz. Esta transparencia genera confianza entre conductores y pasajeros y reduce las disputas por el importe de la tarifa. Permite a los pasajeros elegir la clase de vehículo que prefieren al reservar: opciones económicas con coches compactos y opciones de lujo con sedanes o SUV de alta gama a tarifas premium.

Visualización de rutas y navegación GPS

Ofrecer indicaciones visuales de la ruta hace que la experiencia del taxi sea más fluida tanto para conductores nuevos como experimentados. FiveM ofrece el SetNewWaypoint nativo para la navegación GPS básica, pero una opción más pulida dibuja la ruta directamente en el minimapa y, de forma opcional, muestra una ruta 3D en el mundo. Usa la SetBlipRoute función en los blips de destino para activar la ruta GPS integrada y mejórala con blips personalizados en puntos clave del trayecto. Para beneficio del pasajero, muestra en la interfaz del taxímetro la hora estimada de llegada y la distancia restante, actualizándolas en tiempo real a medida que avanza el vehículo. Aquí tienes cómo configurar el sistema de visualización de rutas con seguimiento de puntos de referencia:

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

Para añadir una capa extra de inmersión, implementa un sistema de calificaciones donde los pasajeros evalúen su viaje según factores como el tiempo de trayecto comparado con la estimación, el número de colisiones durante el viaje y si el conductor respetó las normas de tráfico. Muestra la calificación media del conductor en su perfil de taxi para que los pasajeros puedan verla antes de aceptar el viaje, incentivando así a los conductores a ofrecer un buen servicio.

Sistema de propinas y calificaciones de conductores

Un sistema de propinas añade una mecánica social gratificante que anima a los conductores a ofrecer un servicio excelente. Después de pagar la tarifa y de que el pasajero se baje del vehículo, muestra una solicitud de propina con opciones predeterminadas, como 10%, 15%, 20% o una cantidad personalizada. La propina se entrega directamente al conductor, además de la tarifa base y cualquier comisión de la compañía de taxis. Registra las propinas acumuladas de cada conductor en la base de datos para crear tablas de clasificación y desbloquear bonificaciones para los conductores mejor valorados. En el servidor, procesa las propinas mediante el mismo sistema de transacciones que las tarifas para garantizar la coherencia y evitar duplicados. Los conductores que mantengan una calificación y un promedio de propinas altos a lo largo del tiempo podrían desbloquear ventajas, como acceso a vehículos premium, tasas de comisión reducidas para la compañía o prioridad en la cola de despacho. Combina el sistema de propinas con uno de reseñas donde los pasajeros dejen una breve reseña de texto visible para el conductor, creando un ciclo de comentarios que mejore la calidad general del servicio de taxis en el servidor.

Panel de despacho y solicitudes de viaje

El panel de despacho es el centro donde los taxistas encuentran su próximo trabajo. Créalo como un panel NUI accesible desde la sede de la compañía de taxis o mediante una tecla rápida mientras están de turno. El panel debe mostrar todas las solicitudes de viaje pendientes ordenadas por proximidad, con el nombre del pasajero, el punto de recogida, el destino, la tarifa estimada y la distancia estimada en cada entrada. Cuando un conductor acepta un trabajo, elimínalo del panel para todos los demás conductores e inicia una cuenta regresiva. Si el conductor no llega al punto de recogida dentro del límite de tiempo, el trabajo vuelve al panel y el conductor recibe una penalización de fiabilidad. Para la parte del pasajero, crea una aplicación sencilla para el teléfono o un comando que le permita solicitar un viaje indicando su ubicación actual y su destino. La solicitud se añade a la cola de despacho y el pasajero recibe actualizaciones cuando un conductor acepta y se acerca. Implementa un sistema de cancelación con una pequeña tarifa por cancelaciones tardías para evitar que los pasajeros soliciten y cancelen viajes repetidamente, lo que hace perder tiempo a los conductores y genera una experiencia frustrante.

Gestión de vehículos y sistema de flota

Un sistema de taxis completo necesita una gestión adecuada de vehículos para que los conductores usen los vehículos apropiados y la compañía mantenga su flota. Crea un sistema de entrega de vehículos en la base de taxis donde los conductores puedan elegir entre los vehículos disponibles de la compañía. Registra el kilometraje, el nivel de combustible y el estado de cada vehículo, y responsabiliza a los conductores de devolverlo en condiciones aceptables o de asumir deducciones por reparaciones de sus ganancias. La base debe disponer de diferentes categorías de vehículos: sedanes económicos para tarifas estándar, SUV cómodos para un servicio premium y vehículos de lujo para reservas VIP. Registra las estadísticas de uso de cada vehículo, incluidas las tarifas completadas, los ingresos generados y los costes de mantenimiento. Implementa un sistema de consumo de combustible que obligue a los conductores a repostar en gasolineras durante su turno, añadiendo un coste operativo realista que los conductores experimentados aprenden a gestionar de forma eficiente. Al final del turno, el conductor devuelve el vehículo a la base, se calculan sus ganancias totales menos la comisión de la compañía y los costes por daños al vehículo, y el pago neto se deposita en su cuenta bancaria. Este ciclo completo crea una experiencia laboral realista que refleja las operaciones de una compañía de taxis auténtica.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.