返回博客
Tutorial3 分钟阅读

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的服务器刻时间,以避免影响整体服务器性能。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。