FiveM 出租车系统:脚本编写 NPC 和玩家出租车司机
为FiveM添加一个完整的出租车系统。NPC出租车、玩家司机、计费逻辑、计价器以及适合活跃角色扮演城市的顶级出租车脚本。
Agency Scripts
Agency Scripts 创始人兼首席开发者
出租车工作系统架构
出租车工作系统是任何希望为玩家提供易入门且真实互动的角色扮演服务器的基石功能。与许多枯燥重复的工作不同,设计良好的出租车系统在司机接载乘客、协商车费和在时间压力下穿行城市时创造有机RP体验。架构分为两种模式:玩家对玩家运输,真实乘客请求搭乘;NPC任务模式,系统生成接送地点供非高峰时段单人游玩。两种模式共享相同的车费计算引擎、计价器UI和车辆管理系统。关键设计决策是采用集中调度系统,所有请求进入共享队列供所有值班司机查看,还是基于距离的系统,优先最近司机。多数成功实现采用混合方式,调度板显示所有待处理请求并突出附近请求,司机可自由选择工作,同时确保乘客快速被接载。
出租车计价器实现
计价器是车费系统的核心,需要真实感且对司机和乘客透明。合适的计价器跟踪两个组成部分:每次乘车开始时收取的基础费,以及车辆移动时递增的距离费。一些服务器还添加等待时间费,当出租车静止时按较低的每秒费率收费,模拟现实中交通等待计费。计价器应通过持久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
在乘客请求乘车时显示车费明细,使其了解支付内容。如果启用高峰定价,在 UI 中用不同颜色指示器清晰显示乘数。此透明度建立司机与乘客之间的信任,减少关于车费金额的争议。允许乘客在预订时选择偏好的车辆类别,预算选项使用紧凑型汽车,豪华选项使用高端轿车或 SUV,价格相应较高。
路线显示与GPS导航
提供视觉路线指引,使出租车体验对新手和经验丰富司机更流畅。FiveM 提供 SetNewWaypoint native 用于基础 GPS 路线规划,但更完善的方法是在小地图上直接绘制路线,并可选地在世界中渲染 3D 路径。使用 SetBlipRoute function 用于目的地标记以启用内置 GPS 路径,并在路线关键路点添加自定义标记以增强功能。为了乘客的便利,在仪表界面显示预计到达时间和剩余距离,并随着车辆行进实时更新。以下是设置带路点跟踪的路线显示系统的方法:
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面板,可从出租车公司总部访问,或在值班时通过快捷键打开。调度板应显示所有待处理的乘车请求,按距离排序,每条目显示乘客姓名、上车地点、目的地、预估车费和预估距离。当司机接受任务时,从所有司机的调度板中移除该任务并启动倒计时,如果司机未在规定时间内到达上车点,任务返回调度板,司机将受到可靠性惩罚。乘客端应创建一个简单的手机应用或命令,允许他们通过指定当前位置和目的地来请求乘车。请求进入调度队列,乘客在司机接受并接近时收到更新。实现取消系统,对迟取消收取少量费用,以防止乘客反复请求和取消,浪费司机时间并造成糟糕体验。
车辆管理和车队系统
完整的出租车系统需要合理的车辆管理,确保司机使用合适车辆,公司维护车队。创建出租车车队车辆出库系统,司机从可用公司车辆中选择。每辆车应跟踪里程计数、燃油量和状态,司机负责以可接受状态归还车辆,否则从收入中扣除维修费用。车队应备有不同等级车辆:经济型轿车用于普通乘客,舒适型 SUV 用于高端服务,豪华车用于 VIP 预订。跟踪每辆车的使用统计,包括完成的订单数、收入和维护成本。实现燃油消耗系统,要求司机在班次中途加油,增加真实运营成本,经验丰富的司机会高效管理。班次结束时,司机归还车辆,计算总收入扣除公司佣金和车辆损坏费用,净收入存入银行账户。此完整流程创造了真实的工作体验,模拟实际出租车公司运营。