返回博客
Tutorial4 分钟阅读

FiveM 门锁系统:脚本实现安全门锁与钥匙

为任何FiveM服务器添加门锁。配置、钥匙、仅限特定职业门以及适用于警察局、住宅和商业内部的最佳门锁脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

为什么门锁在角色扮演中重要

门锁系统是任何严肃FiveM角色扮演服务器中最被低估但又必不可少的组件之一。没有适当的门管理,玩家可以无限制地进入警察军械库、医院员工室和帮派藏身处,完全破坏沉浸感。一个良好实现的门锁系统允许服务器所有者控制地图上每个内部区域的访问权限,从政府建筑到玩家拥有的商铺。它创造了自然的边界,推动角色扮演互动,如钥匙交换、开锁场景和协调突袭。在本指南中,我们将通过使用ox_doorlock作为基础构建完整的门锁系统,然后通过自定义开锁机制、门禁卡认证、门组和特定业务访问控制进行扩展。

设置 ox_doorlock

ox_doorlock资源是FiveM生态中最广泛采用的门锁解决方案,提供简洁API、内置管理员UI用于放置门,并支持双门和车库式动画门。在编写任何自定义代码前,需要坚实基础。安装ox_doorlock及其依赖的ox_lib,后者提供UI组件和实用函数。你的 server.cfg 需要确保资源按正确顺序启动,先运行数据库,然后是 ox_lib,最后是 ox_doorlock。启动后,内置管理员命令允许你走到游戏世界中的任何门前,点击注册。

-- server.cfg load order
ensure oxmysql
ensure ox_lib
ensure ox_doorlock

-- Grant admin access for door placement
add_ace group.admin command.doorlock allow

通过管理员界面注册门后,ox_doorlock将每条记录存储在数据库中,包括坐标、朝向、模型哈希和锁定状态。您可以通过程序查询和操作这些数据。该资源提供导出和事件用于切换锁定、检查状态和管理访问。在基于此构建自定义功能之前,理解数据结构至关重要。

-- Checking if a specific door is locked
local doorId = 1
local isLocked = exports.ox_doorlock:getDoorState(doorId)

-- Toggle a door lock from server side
exports.ox_doorlock:setDoorState(doorId, not isLocked)

-- Listen for door state changes
AddEventHandler('ox_doorlock:stateChanged', function(id, state, source)
    print(('Door %d changed to %s by player %s'):format(id, state and 'locked' or 'unlocked', source))
end)

构建开锁机制

开锁为犯罪角色增加了游戏玩法层,使锁门变得有意义。门不再是绝对障碍,而是基于技能的挑战。最佳方法是一个需要时机和精准的小游戏,使结果感觉是靠技巧获得而非随机。我们将使用ox_lib内置的技能检测系统创建一个开锁流程,要求玩家背包中有开锁工具,触发动画,并运行多阶段技能检测。技能检测失败应有后果,如开锁工具损坏或通过调度事件警告附近警察。

-- client/lockpicking.lua
local function AttemptLockpick(doorId)
    local hasLockpick = exports.ox_inventory:Search('count', 'lockpick')
    if hasLockpick < 1 then
        lib.notify({ title = 'No Lockpick', type = 'error' })
        return
    end

    -- Play lockpicking animation
    local ped = PlayerPedId()
    TaskPlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandlenry', 8.0, -8.0, -1, 1, 0, false, false, false)

    -- Multi-stage skillcheck: easy, medium, hard
    local success = lib.skillCheck({'easy', 'medium', 'hard'}, {'w', 'a', 's', 'd'})

    ClearPedTasks(ped)

    if success then
        TriggerServerEvent('doorlock:lockpick:success', doorId)
        lib.notify({ title = 'Lock Picked', description = 'The door clicks open.', type = 'success' })
    else
        TriggerServerEvent('doorlock:lockpick:fail', doorId)
        lib.notify({ title = 'Failed', description = 'The lockpick snapped.', type = 'error' })
    end
end

在服务器端,你需要验证开锁尝试,失败时移除开锁工具,成功时解锁门,并可选择派发警察警报。限速很重要以防止刷屏尝试。为每个玩家每扇门存储冷却时间,防止失败后立即重试。服务器处理程序还应验证玩家确实靠近他们声称要开锁的门,防止远程利用。

-- server/lockpicking.lua
local cooldowns = {}

RegisterNetEvent('doorlock:lockpick:success', function(doorId)
    local src = source
    local playerCoords = GetEntityCoords(GetPlayerPed(src))

    -- Verify proximity to door (anti-cheat)
    local doorData = exports.ox_doorlock:getDoor(doorId)
    if not doorData then return end
    if #(playerCoords - doorData.coords) > 3.0 then return end

    -- Check cooldown
    local key = ('%s:%s'):format(src, doorId)
    if cooldowns[key] and os.time() - cooldowns[key] < 30 then return end

    exports.ox_doorlock:setDoorState(doorId, 0) -- Unlock
    cooldowns[key] = os.time()

    -- Auto-relock after 60 seconds
    SetTimeout(60000, function()
        exports.ox_doorlock:setDoorState(doorId, 1)
    end)
end)

RegisterNetEvent('doorlock:lockpick:fail', function(doorId)
    local src = source
    exports.ox_inventory:RemoveItem(src, 'lockpick', 1)

    -- Alert police dispatch
    TriggerEvent('dispatch:alert', {
        coords = GetEntityCoords(GetPlayerPed(src)),
        message = 'Attempted break-in reported',
        code = '10-31',
        job = 'police'
    })
end)

实现钥匙卡系统

钥匙卡提供了比简单钥匙物品更结构化的访问控制机制。它们适用于企业大楼、政府设施和需要与特定权限等级绑定的受限区域。钥匙卡系统为每扇门分配1到5的安全等级,玩家必须持有相同或更高级别的钥匙卡才能解锁。这创造了自然的等级体系,三级钥匙卡可打开所有1、2、3级门,但无法进入4或5级受限区。钥匙卡可由职业系统发放、作为战利品获得或通过进阶系统制作。

-- shared/config.lua
Config = {}
Config.KeycardLevels = {
    { name = 'keycard_1', label = 'Green Keycard',  level = 1 },
    { name = 'keycard_2', label = 'Blue Keycard',   level = 2 },
    { name = 'keycard_3', label = 'Yellow Keycard', level = 3 },
    { name = 'keycard_4', label = 'Red Keycard',    level = 4 },
    { name = 'keycard_5', label = 'Black Keycard',  level = 5 },
}

Config.DoorSecurity = {
    [10] = { level = 1, name = 'Office Lobby' },
    [11] = { level = 2, name = 'Server Room' },
    [12] = { level = 3, name = 'Executive Floor' },
    [13] = { level = 4, name = 'Vault Anteroom' },
    [14] = { level = 5, name = 'Main Vault' },
}

-- client/keycard.lua
local function TryKeycardAccess(doorId)
    local security = Config.DoorSecurity[doorId]
    if not security then return false end

    for _, card in ipairs(Config.KeycardLevels) do
        if card.level >= security.level then
            local count = exports.ox_inventory:Search('count', card.name)
            if count > 0 then
                -- Play card swipe animation
                lib.requestAnimDict('anim@heists@keycard@')
                TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
                Wait(1200)
                ClearPedTasks(PlayerPedId())
                return true, card
            end
        end
    end
    return false
end

门组与业务访问

当服务器有数百个上锁的门时,单独管理每扇门变得不可扩展。门组解决此问题,允许你将多扇门分配到一个命名组,并一次性控制整个组的访问权限。警察局可能有 15 扇上锁的门,所有门都应由警察职业访问。你无需单独配置每扇门,只需将它们全部分配到“police_station”组,并基于职业名称授予访问权限。当管理员雇佣新警员时,他们自动获得该组所有门的访问权限,无需手动配置。

-- server/door_groups.lua
local DoorGroups = {
    police_station = {
        doors = {1, 2, 3, 4, 5, 6, 7, 8},
        access = {
            { type = 'job', name = 'police', minGrade = 0 },
            { type = 'job', name = 'sheriff', minGrade = 0 },
        }
    },
    pillbox_hospital = {
        doors = {20, 21, 22, 23, 24},
        access = {
            { type = 'job', name = 'ambulance', minGrade = 0 },
            { type = 'job', name = 'doctor', minGrade = 2 },
        }
    },
    vangelico = {
        doors = {30, 31},
        access = {
            { type = 'job', name = 'jeweler', minGrade = 0 },
            { type = 'item', name = 'vangelico_key' },
        }
    },
}

function HasGroupAccess(source, groupName)
    local group = DoorGroups[groupName]
    if not group then return false end

    for _, rule in ipairs(group.access) do
        if rule.type == 'job' then
            local job = GetPlayerJob(source)
            if job and job.name == rule.name and job.grade >= (rule.minGrade or 0) then
                return true
            end
        elseif rule.type == 'item' then
            local count = exports.ox_inventory:GetItem(source, rule.name, nil, true)
            if count and count > 0 then return true end
        end
    end
    return false
end

业务门通过将门访问权限与所有权记录绑定,增加了另一层维度。当玩家购买业务时,应自动获得该物业所有门的控制权。系统需要实时检查业务所有权,因为物业可能被出售或转让。实现一个回调,查询你的业务表以验证当前所有者,然后用短TTL缓存结果,以避免每次门交互时频繁访问数据库。

跨客户端同步门状态

门同步是门锁系统中较为棘手的方面之一。当一名玩家解锁门时,所有附近玩家都需要看到门打开。FiveM原生门控是基于每个客户端独立管理门状态的,这意味着每个客户端独立处理门状态。没有适当同步时,一名玩家看到门打开,而另一名玩家看到门关闭。ox_doorlock处理基本状态同步,但自定义扩展需要谨慎处理状态传播。使用状态包或服务器权威事件向合理范围内的所有玩家广播门状态变化。避免对整个服务器同步每次门切换,因为这会在拥有数百扇门的大型服务器上产生不必要的网络开销。

-- server: broadcast door state to nearby players only
local function SyncDoorToNearby(doorId, state, coords, range)
    range = range or 100.0
    local players = GetPlayers()
    for _, playerId in ipairs(players) do
        local ped = GetPlayerPed(playerId)
        if ped and DoesEntityExist(ped) then
            local playerCoords = GetEntityCoords(ped)
            if #(playerCoords - coords) <= range then
                TriggerClientEvent('doorlock:sync', tonumber(playerId), doorId, state)
            end
        end
    end
end

-- client: apply synced door state
RegisterNetEvent('doorlock:sync', function(doorId, state)
    local door = exports.ox_doorlock:getDoor(doorId)
    if door then
        door.state = state
    end
end)

安全注意事项与防作弊

门锁系统是利用者常见的攻击目标,因为绕过门锁通常能获得受限物品、武器或金钱。每个门状态切换请求必须在服务器端验证。绝不信任客户端发送的门ID,必须核实玩家的接近度和访问权限。对每次门状态变更进行日志记录,方便管理员审查异常行为,比如玩家解锁不应访问的保险库门。对所有门交互进行速率限制,防止暴力破解开锁系统。建议添加服务器端事件日志,记录玩家 Steam ID、门 ID、时间戳及访问方式以备审计。这些日志在调查漏洞或解决玩家间关于访问权限的争议时极为重要。

整合所有内容

生产就绪的门锁系统将所有组件整合到统一框架。入口点是目标系统交互,支持 ox_target 或 qb-target,检测玩家观察注册门时,基于其访问权限显示上下文选项。若拥有职位权限,显示解锁按钮;若持有钥匙卡,显示刷卡选项;若持有锁匠工具且门标记为可开锁,显示开锁选项。每条路径连接到相应处理器,在服务器验证并同步结果给附近客户端。配置应通过数据库条目或共享配置文件实现数据驱动,方便服务器所有者添加、移除和修改门访问权限,无需触及代码。模块化架构允许替换开锁小游戏、添加生物识别扫描等新访问方式,或集成完全不同的库存系统,而无需重写核心逻辑。

准备好开始了吗?

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