FiveM停车系统:真实车辆停车脚本
为您的FiveM城市添加真实停车系统。街边停车、付费停车场、扣押规则以及防止车辆杂乱的最佳停车脚本。
Agency Scripts
Agency Scripts 创始人兼首席开发者
理解停车系统架构
停车系统通过为玩家提供指定区域停放车辆并对违规行为施加后果,为任何 FiveM 角色扮演服务器增加深度和现实感。其基础是跟踪哪些停车位被占用,监控车辆停放时间,并通过计时器或许可证处理付款。系统包括服务器端逻辑用于状态跟踪和支付处理,客户端检测停车区域的进入和退出,以及可选的 NUI 界面显示计时器状态和支付选项。一个良好实现的停车系统还应与现有的警察和拖车脚本集成,以便授权玩家对非法停车车辆开罚单或扣押。
定义停车区和车位
停车区域定义为地图上的多边形或盒状区域,每个区域包含具有特定坐标和朝向的停车位。使用配置文件定义这些区域,使服务器管理员无需修改代码即可添加新停车区。每个区域可有不同规则,如免费停车、计时停车或仅限许可区域。以下是实用的区域配置结构:
Config.ParkingZones = {
['pillbox_lot'] = {
label = 'Pillbox Medical Parking',
type = 'metered', -- 'free', 'metered', 'permit'
rate = 50, -- $ per hour (metered only)
maxTime = 120, -- minutes max parking
blip = {sprite = 357, color = 3, scale = 0.6},
spots = {
{coords = vector4(310.5, -590.2, 43.3, 70.0), occupied = false},
{coords = vector4(313.8, -591.6, 43.3, 70.0), occupied = false},
{coords = vector4(317.1, -593.0, 43.3, 70.0), occupied = false},
{coords = vector4(320.4, -594.4, 43.3, 70.0), occupied = false},
},
fineAmount = 250, -- fine for overtime or illegal parking
},
['legion_street'] = {
label = 'Legion Square Parking',
type = 'metered',
rate = 75,
maxTime = 60,
blip = {sprite = 357, color = 3, scale = 0.6},
spots = {
{coords = vector4(215.2, -810.5, 30.7, 160.0), occupied = false},
{coords = vector4(212.6, -810.5, 30.7, 160.0), occupied = false},
},
fineAmount = 350,
},
}
该 vector4 格式存储x、y、z坐标及朝向角度,确保车辆停放时面向正确方向。 occupied 该标志由服务器在运行时管理,重启时不保存,因为车辆通过车库系统数据库单独跟踪。保持每个地点的车位数量合理,因为在小区域内车位过多会导致车辆穿模问题。
停车计时器系统
计时器系统跟踪每辆车停放时间并据此收费。玩家在计时区停车时,需与计时器交互启动计时,选择时长并预付费用。服务器存储会话开始时间、付费时长、车牌号和区域ID。服务器端定时线程定期检查所有活跃计时会话,标记过期会话以便执法。核心计时逻辑如下:
local ActiveMeters = {} -- keyed by plate
RegisterNetEvent('parking:server:startMeter', function(plate, zoneId, duration)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local zone = Config.ParkingZones[zoneId]
if not zone or zone.type ~= 'metered' then return end
-- Calculate cost based on duration in minutes
local hours = duration / 60
local cost = math.ceil(hours * zone.rate)
if Player.Functions.RemoveMoney('cash', cost, 'parking-meter') then
ActiveMeters[plate] = {
zone = zoneId,
startTime = os.time(),
paidUntil = os.time() + (duration * 60),
plate = plate,
owner = Player.PlayerData.citizenid,
}
TriggerClientEvent('parking:client:meterStarted', src, duration, cost)
else
TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
end
end)
-- Check for expired meters every 60 seconds
CreateThread(function()
while true do
Wait(60000)
local now = os.time()
for plate, meter in pairs(ActiveMeters) do
if now > meter.paidUntil then
-- Meter expired, issue fine
IssueParkingFine(plate, meter.zone, meter.owner)
ActiveMeters[plate] = nil
end
end
end
end)
60秒的计时器分辨率足以满足停车执法需求,同时避免不必要的CPU负载。计价器会话存储在内存中而非数据库,因为它们是临时的,不需在服务器重启后保留。服务器重启时,所有计价器重置,这可以接受,因为大多数框架中车辆也会在重启时返回车库。
车辆检测和位置分配
检测车辆进入或离开停车位需要客户端定期进行接近检测。使用基于距离的检测,针对附近区域的所有停车位,而不是检查整个地图上的每个停车位。仅在玩家处于车辆内且靠近配置的停车区域时运行检测循环。这可以在正常游戏过程中保持CPU使用率最低:
-- client/detection.lua
local nearestZone = nil
local currentSpot = nil
CreateThread(function()
while true do
local sleep = 1000
local ped = PlayerPedId()
local inVehicle = IsPedInAnyVehicle(ped, false)
if inVehicle then
local veh = GetVehiclePedIsIn(ped, false)
local vehCoords = GetEntityCoords(veh)
for zoneId, zone in pairs(Config.ParkingZones) do
local firstSpot = zone.spots[1]
local zoneDist = #(vehCoords - vector3(firstSpot.coords.x,
firstSpot.coords.y, firstSpot.coords.z))
if zoneDist < 50.0 then
sleep = 200
nearestZone = zoneId
for i, spot in ipairs(zone.spots) do
local spotPos = vector3(spot.coords.x, spot.coords.y, spot.coords.z)
local dist = #(vehCoords - spotPos)
if dist < 3.0 then
local speed = GetEntitySpeed(veh)
if speed < 0.5 then
currentSpot = {zone = zoneId, index = i}
ShowParkingPrompt(zone)
end
end
end
break
end
end
else
nearestZone = nil
currentSpot = nil
end
Wait(sleep)
end
end)
0.5的速度检测确保停车提示仅在车辆几乎静止时出现,防止玩家驶过停车场时界面闪烁。靠近区域时200ms的自适应睡眠间隔与其他地方1000ms平衡了响应速度和性能。对于拥有大量停车区的服务器,建议使用空间哈希或四叉树,避免每个tick遍历所有区域。
停车罚款与执法系统
当计时器到期或车辆非法停车时,系统会向车辆所有者发出罚款。罚款存储在数据库表中,并在车主下次登录或取车时显示。警察玩家也可以通过命令或目标交互手动开具停车罚单。罚款记录包括车牌、金额、开具原因和时间戳:
function IssueParkingFine(plate, zoneId, citizenid)
local zone = Config.ParkingZones[zoneId]
local amount = zone and zone.fineAmount or 250
MySQL.insert([[
INSERT INTO parking_fines (plate, citizenid, amount, reason, zone, issued_at)
VALUES (?, ?, ?, ?, ?, NOW())
]], {plate, citizenid, amount, 'Expired parking meter', zoneId})
-- Notify owner if online
local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
if Player then
TriggerClientEvent('QBCore:Notify', Player.PlayerData.source,
'Parking fine issued: $' .. amount, 'error')
end
end
-- Police manual ticket command
RegisterNetEvent('parking:server:issueTicket', function(plate, reason)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
if Player.PlayerData.job.name ~= 'police' then return end
local vehicle = MySQL.query.await(
'SELECT citizenid FROM player_vehicles WHERE plate = ?', {plate}
)
if vehicle and vehicle[1] then
IssueParkingFine(plate, 'manual', vehicle[1].citizenid)
TriggerClientEvent('QBCore:Notify', src, 'Ticket issued for ' .. plate, 'success')
end
end)
将罚款系统与车辆取回流程集成,要求玩家在取车前支付未缴罚款。这形成了自然的执法循环,忽视停车规则会产生真实的游戏内后果。考虑在计时器到期后设置五到十分钟的宽限期再开罚单,这模拟现实世界的停车执法,减少玩家因精准罚单带来的挫败感。
NUI仪表显示和支付界面
停车计时器NUI应简洁且信息丰富,显示当前区域名称、每小时费率、已付款剩余时间,以及选择时长和付款按钮。计时器激活时显示每秒更新的倒计时,让玩家准确知道剩余时间。采用紧凑设计,不遮挡游戏视图,类似现实停车计时器屏幕。计时器显示颜色从绿变黄再变红,随着时间耗尽提供直观视觉提示,无需玩家读取具体剩余分钟。保持NUI轻量,使用原生JavaScript和最少CSS,避免加载时间长的框架以保持界面简洁。
拖车集成与遗弃车辆
将您的停车系统与拖车机制连接,以便过期或非法停放的车辆可以被拖到扣押场。当车辆违规时间超过配置的阈值时,在服务器状态中将其标记为可拖车。拖车工作玩家随后可以在地图上看到这些车辆的高亮标记,并通过拖车获得报酬。拖车过程应将车辆状态从数据库中的“out”更新为“impounded”,并记录扣押原因作为停车违规。对于长时间无人操作的遗弃车辆,实施自动清理程序,在配置的超时后将其移至扣押场,为活跃玩家释放停车位。这防止停车场被长时间未登录的玩家车辆占满。
性能与可扩展性考虑
停车系统运行持续检测循环,若未优化会影响性能。客户端车辆检测应仅对渲染距离内的区域激活,服务器端计时器检查线程应批量处理所有活动会话,而非为每辆车创建单独定时器。对停车罚款数据库表的车牌和 citizenid 列建立索引,确保玩家取车或管理员查询罚款历史时快速检索。对于拥有数十个停车区域和数百辆同时车辆的服务器,建议将区域数据缓存到 Lua 表中,而非每次检查都读取配置,并使用原生函数 GetClosestVehicle 谨慎使用,因为它扫描范围内所有车辆。在高峰负载下使用FiveM内置分析器对你的停车资源进行性能分析以识别热点,记住停车系统应使用少于0.1ms的服务器刻时间,以避免影响整体服务器性能。