返回博客
Tutorial2 分钟阅读

FiveM 车库系统:存储、生成及扣押车辆

设计一个坚固可靠的 FiveM 车库系统。个人和工作车库、扣押流程、钥匙、库存关联及顶级免费和付费车库脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

理解车库系统架构

车库系统是任何FiveM角色扮演服务器最基本的功能之一,是玩家存储、取回和管理车辆的主要方式。核心由三层互联组成:数据库层保存车辆所有权和状态,服务器端逻辑层处理生成和销毁操作及验证,客户端UI层让玩家与存储车辆交互。编写代码前需决定关键架构选择,如车库是否基于位置或全局,玩家是否能访问所有车库或仅特定车库,以及如何处理车辆属性如改装、燃油和损坏状态。最佳车库系统保存完整车辆属性对象,使玩家取车时车辆保持原状,包括自定义涂装、性能升级甚至车身污垢。

数据库架构与车辆持久化

您的数据库架构构成整个车库系统的基础。您需要一个表来跟踪车辆所有权、当前状态和存储属性。状态列至关重要,因为它决定车辆当前是在世界中生成、存放在车库还是停放在扣押场。以下是涵盖关键字段的实用架构:

CREATE TABLE IF NOT EXISTS player_vehicles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    citizenid VARCHAR(50) NOT NULL,
    vehicle VARCHAR(50) NOT NULL,
    hash VARCHAR(50) NOT NULL,
    mods LONGTEXT DEFAULT '{}',
    plate VARCHAR(8) NOT NULL,
    fakeplate VARCHAR(8) DEFAULT NULL,
    garage VARCHAR(50) DEFAULT 'pillboxgarage',
    fuel INT DEFAULT 100,
    engine FLOAT DEFAULT 1000.0,
    body FLOAT DEFAULT 1000.0,
    state INT DEFAULT 1,  -- 0 = out, 1 = garaged, 2 = impounded
    depotprice INT DEFAULT 0,
    drivingdistance INT DEFAULT 0,
    INDEX idx_citizenid (citizenid),
    INDEX idx_plate (plate),
    INDEX idx_state (state)
);

mods 列存储一个 JSON 编码的对象,包含所有由类似函数返回的车辆改装信息 QBCore.Functions.GetVehicleProperties(vehicle) 或ESX中的等效项。索引 citizenid, plate,和 state 列确保即使您的玩家基数增长到数千人,查询仍然快速。与此表交互时始终使用参数化查询以防止 SQL 注入攻击。

服务器端生成和销毁逻辑

服务器端是所有关键验证发生的地方。当玩家请求从车库取车时,服务器必须验证玩家确实拥有该车辆,车辆当前处于车库状态,并且有有效的生成点可用。绝不允许客户端直接决定生成位置,因为作弊者可能在地图任意位置生成车辆。相反,在服务器定义生成点,并选择最近的可用点。以下是一个安全的服务器端取车处理示例:

RegisterNetEvent('garage:server:takeVehicle', function(vehicleId, garageId)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local citizenid = Player.PlayerData.citizenid
    local result = MySQL.query.await(
        'SELECT * FROM player_vehicles WHERE id = ? AND citizenid = ? AND state = 1',
        {vehicleId, citizenid}
    )

    if not result or not result[1] then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle not found', 'error')
        return
    end

    local vehData = result[1]
    local spawnPoint = GetAvailableSpawnPoint(garageId)

    if not spawnPoint then
        TriggerClientEvent('QBCore:Notify', src, 'No parking spots available', 'error')
        return
    end

    MySQL.update('UPDATE player_vehicles SET state = 0 WHERE id = ?', {vehicleId})
    TriggerClientEvent('garage:client:spawnVehicle', src, vehData, spawnPoint)
end)

对于消失过程,服务器需要在移除车辆前捕获当前车辆属性,确保自上次存储以来的修改被保存。始终更新燃油、引擎健康和车体健康,同时更新mods JSON,确保所有数据正确持久化。在服务器端实现距离检查,确保玩家确实靠近车库位置后才允许存储操作。

带NUI的客户端车库界面

车库 UI 是玩家与系统交互的地方,设计良好的界面决定体验是令人沮丧还是流畅。使用 NUI 结合 HTML、CSS 和 JavaScript 构建响应式面板,显示当前车库中存储的所有车辆。每个车辆条目应一目了然地显示车辆名称、车牌、燃油水平和整体状况。包含预览系统,临时生成车辆模型,方便玩家查看所选车辆,尤其当玩家拥有多辆同类型车辆时非常有用。以下是打开车库菜单和收集车辆数据的客户端逻辑:

RegisterNetEvent('garage:client:openMenu', function(garageId)
    QBCore.Functions.TriggerCallback('garage:server:getVehicles', function(vehicles)
        if not vehicles or #vehicles == 0 then
            QBCore.Functions.Notify('No vehicles stored here', 'info')
            return
        end

        SetNuiFocus(true, true)
        SendNUIMessage({
            action = 'openGarage',
            vehicles = vehicles,
            garageName = Config.Garages[garageId].label
        })
    end, garageId)
end)

RegisterNUICallback('takeVehicle', function(data, cb)
    SetNuiFocus(false, false)
    TriggerServerEvent('garage:server:takeVehicle', data.vehicleId, currentGarage)
    cb('ok')
end)

在JavaScript端,将每辆车渲染为带有取车或转移到其他车库操作按钮的卡片。考虑添加排序和筛选选项,方便拥有大量车辆的玩家快速找到所需车辆。一个按车牌号或车辆名称筛选的搜索栏是小改进,但极大提升了在车辆积累多的服务器上的可用性。

车辆属性存储和恢复

正确保存和恢复车辆属性是车库系统开发中最棘手的部分之一。属性对象包含数十个字段,包括颜色、涂装、霓虹灯、车窗色调、轮胎烟雾颜色、额外配件和所有性能改装。存储车辆时,应在删除实体前立即捕获属性以确保获取最新状态。重新生成车辆时,需要等待实体完全加载后再应用属性,否则自定义轮毂或引擎升级等改装将默默失败。使用短暂延迟或适当的实体存在检查循环:

function SpawnAndApplyMods(vehData, spawnPoint)
    local model = GetHashKey(vehData.vehicle)
    RequestModel(model)

    while not HasModelLoaded(model) do
        Wait(10)
    end

    local veh = CreateVehicle(model, spawnPoint.x, spawnPoint.y, spawnPoint.z,
        spawnPoint.w, true, false)

    while not DoesEntityExist(veh) do
        Wait(10)
    end

    local props = json.decode(vehData.mods)
    if props then
        QBCore.Functions.SetVehicleProperties(veh, props)
    end

    SetVehicleFuelLevel(veh, vehData.fuel + 0.0)
    SetVehicleEngineHealth(veh, vehData.engine + 0.0)
    SetVehicleBodyHealth(veh, vehData.body + 0.0)
    SetEntityAsMissionEntity(veh, true, true)
    SetModelAsNoLongerNeeded(model)
    TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1)
end

特别注意附加车辆,因为它们有时具有与原版 GTA 车辆行为不同的自定义附加组件或涂装索引。使用各种车辆类型彻底测试你的财产保存和恢复周期,以尽早发现边缘情况。

扣押系统集成

扣押系统与您的车库协同工作,增加角色扮演服务器所需的现实感。车辆因多种原因被扣押:逮捕时警方扣押、服务器重启后自动清理遗弃车辆,或管理员因违规操作。车辆被扣押时,在数据库中将状态更新为2,并可选择设置取车费用,玩家需支付后才能取回。扣押场应类似车库功能,但增加支付费用后才能释放的要求。在地图上创建独立扣押点,设有专属生成点和NUI界面,显著显示扣押费用。

RegisterNetEvent('police:server:impoundVehicle', function(plate, price)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Verify the player has police job authorization
    if Player.PlayerData.job.name ~= 'police' then return end

    local result = MySQL.update.await(
        'UPDATE player_vehicles SET state = 2, depotprice = ? WHERE plate = ?',
        {price or 500, plate}
    )

    if result > 0 then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle impounded', 'success')
    end
end)

考虑实施分级定价系统,扣押费用每次同一车辆被扣押时递增,防止玩家将扣押场当作免费停车场。您还可以添加基于时间的机制,车辆在扣押场停放超过配置的现实天数后自动免费释放回车库,防止玩家因永久丢失而沮丧。

车库标记和目标集成

让车库易于发现和交互需要正确的地图标记和交互区域。为每个车库位置添加地图标记,方便玩家在小地图上找到,并使用基于接近的标记或目标系统集成作为交互触发。像 ox_target 或 qb-target 这样的目标系统提供更清洁的体验,因为它们仅在玩家瞄准特定点时显示交互选项,减少屏幕杂乱。在客户端和服务器都能引用的共享配置文件中定义车库位置,保持坐标、生成点和设置同步:

Config.Garages = {
    ['pillboxgarage'] = {
        label = 'Pillbox Garage',
        coords = vector3(215.83, -810.18, 30.73),
        spawnPoints = {
            vector4(218.32, -803.28, 30.73, 248.5),
            vector4(222.41, -799.84, 30.73, 248.5),
            vector4(226.52, -796.41, 30.73, 248.5),
        },
        blip = { sprite = 357, color = 3, scale = 0.7 },
        vehicleType = 'car',  -- car, boat, aircraft
    },
}

通过为船只和飞机创建单独车库并在靠近水域或机场的适当生成点支持多种车辆类型。 vehicleType 过滤器确保玩家在街道车库只看到陆地车辆,在码头只看到船只,避免混淆和生成问题。当玩家接近车库时,检查他们是否有车辆存放在那里,避免对无车辆玩家显示交互提示,防止不必要的菜单打开。

性能优化技巧

车库系统如果实现不当,尤其是在拥有数百名同时在线玩家且每人拥有多辆车辆的服务器上,可能成为性能瓶颈。在服务器端缓存车辆列表,而不是每次玩家打开车库菜单时查询数据库,仅在车辆状态变化时使缓存失效。在客户端,避免在不需要时保持NUI框架打开,因为即使隐藏的NUI框架运行JavaScript定时器或动画也会消耗资源。生成车辆时,确保在玩家存车后正确清理实体,将其标记为不再需要,并实现定期运行的备用清理程序,以捕获因崩溃或断线未正确销毁的孤立车辆实体。使用原生函数如 GetGamePool('CVehicle') 谨慎使用并缓存结果,当你需要检查世界中现有玩家车辆时。最后,考虑为每个车库实现最大车辆限制,以保持数据库查询有界,防止单个玩家存储数百辆车导致检索操作变慢。

准备好开始了吗?

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