返回博客
Guide4 分钟阅读

FiveM 调试技巧:快速定位 Lua 和 JS 中的错误

更快调试 FiveM 脚本。服务器日志、客户端打印、txAdmin 工具、性能分析技巧和经过验证的工作流程,帮助你在玩家察觉前修复问题。

Agency Scripts

Agency Scripts 创始人兼首席开发者

FiveM脚本调试艺术

调试 FiveM 脚本与调试标准应用程序根本不同。你面对的是分离的客户端-服务器架构,代码运行在两个独立的运行时,事件跨网络传输,游戏状态每帧变化。当出现问题时,错误可能源自服务器却在客户端表现,反之亦然。建立系统化的调试方法将节省你无数时间,并显著加快开发流程。本指南涵盖专业 FiveM 开发者每日使用的核心工具、技术和模式。

有效使用打印语句

FiveM中最基本的调试工具是 print() function,但有效使用它不仅仅是打印变量。用前缀结构化调试输出,标识脚本、客户端或服务器端以及打印发生的函数。使用颜色代码使重要消息在控制台中突出显示。构建一个可开关的调试工具,无需从代码中移除调试行。

-- shared/debug.lua
local DEBUG_ENABLED = GetConvar('myresource_debug', 'false') == 'true'
local RESOURCE_NAME = GetCurrentResourceName()

function DebugLog(module, message...)
    if not DEBUG_ENABLED then return end

    local side = IsDuplicityVersion() and 'SERVER' else 'CLIENT'
    local formatted = type(message) == 'string' and message:format(...) or tostring(message)
    local timestamp = os.date('%H:%M:%S')

    print(('[^3%s^0][^5%s^0][^2%s^0] %s'):format(
        timestamp, RESOURCE_NAME, side .. ':' .. module, formatted
    ))
end

function DebugTable(module, tbl, depth)
    if not DEBUG_ENABLED then return end
    depth = depth or 0
    local indent = string.rep('  ', depth)

    if type(tbl) ~= 'table' then
        DebugLog(module, '%s%s', indent, tostring(tbl))
        return
    end

    for k, v in pairs(tbl) do
        if type(v) == 'table' then
            DebugLog(module, '%s%s = {', indent, tostring(k))
            DebugTable(module, v, depth + 1)
            DebugLog(module, '%s}', indent)
        else
            DebugLog(module, '%s%s = %s (%s)', indent, tostring(k), tostring(v), type(v))
        end
    end
end

使用调试工具

有了这个实用工具,您可以在脚本中添加调试日志记录,在生产环境中完全静默。通过添加启用它 set myresource_debug true 需要排查时发送到您的服务器配置。结构化输出便于在控制台日志中搜索并准确找到所需内容。

-- server/jobs.lua
RegisterNetEvent('myresource:startJob', function(jobName)
    local src = source
    DebugLog('jobs', 'Player %d attempting to start job: %s', src, jobName)

    local playerData = GetPlayerData(src)
    DebugTable('jobs', playerData)

    if not playerData then
        DebugLog('jobs', 'ERROR: No player data found for source %d', src)
        return
    end

    if playerData.job == jobName then
        DebugLog('jobs', 'Player %d already has job %s, skipping', src, jobName)
        return
    end

    DebugLog('jobs', 'Job %s assigned to player %d successfully', jobName, src)
end)

常见 FiveM 错误及解决方案

SCRIPT ERROR: 尝试索引 nil 值

这是FiveM开发中最常见的Lua错误。它意味着您尝试访问的变量是 nil. 最常见原因是访问玩家数据时数据尚未加载,调用您使用版本中不存在的框架函数,或读取配置时键名拼写错误。访问嵌套属性前务必检查 nil。

-- BAD: Will crash if GetPlayerData returns nil
local name = GetPlayerData(src).charinfo.firstname

-- GOOD: Defensive nil checks
local playerData = GetPlayerData(src)
if not playerData then
    print('[ERROR] Player data is nil for source: ' .. src)
    return
end

local charinfo = playerData.charinfo
if not charinfo then
    print('[ERROR] charinfo missing for source: ' .. src)
    return
end

local name = charinfo.firstname or 'Unknown'

SCRIPT ERROR: 尝试调用 nil 值

当你尝试调用不存在的函数时会出现此错误。在FiveM中,这通常发生在调用尚未启动的资源导出、使用更新中重命名的框架函数或忘记在清单中加载共享文件时。检查你的 fxmanifest.lua 确保所有必需文件都列出且顺序正确。

-- Safely calling an export that might not be available
local function SafeExport(resource, exportName...)
    local success, result = pcall(function(...)
        return exports[resource][exportName](...)
    end...)

    if not success then
        print(('[^1ERROR^0] Failed to call export %s:%s - %s'):format(
            resource, exportName, tostring(result)
        ))
        return nil
    end

    return result
end

-- Usage
local inventory = SafeExport('ox_inventory', 'GetInventory', src)

事件未注册

触发未注册处理程序的事件时,FiveM 会静默丢弃且控制台无错误提示。调试时极其令人沮丧,因为一切看似正常但无任何反应。开发时使用辅助函数验证事件已注册,并在事件触发周围添加日志。

-- server/debug_events.lua
-- Wrap TriggerClientEvent to log when events fire
local originalTrigger = TriggerClientEvent

if GetConvar('myresource_debug', 'false') == 'true' then
    TriggerClientEvent = function(eventName, target...)
        print(('[^3EVENT^0] TriggerClientEvent: %s -> target: %s'):format(
            eventName, tostring(target)
        ))
        return originalTrigger(eventName, target...)
    end
end

使用DevTools进行NUI调试

对于带有NUI界面的脚本,内置的Chromium开发者工具非常宝贵。通过F8控制台输入打开 nui_devtools 访问完整 Chrome 检查器。这提供了用于检查 DOM 结构的 Elements 面板、用于 JavaScript 错误的控制台、用于资源加载的网络标签以及用于设置断点的 Sources 面板。对于 NUI 通信问题,请记录消息桥的双方。

// nui/js/debug.js
// Log all incoming NUI messages
window.addEventListener('message', (event) => {
    if (event.data && event.data.action) {
        console.log(
            '%c[NUI Received]%c ' + event.data.action,
            'background: #2dd4bf; color: #000; padding: 2px 6px; border-radius: 3px;',
            'color: #94a3b8;',
            event.data
        );
    }
});

// Wrap fetch to log NUI callbacks
const originalFetch = window.fetch;
window.fetch = function(url, options) {
    const body = options?.body ? JSON.parse(options.body) : null;
    console.log(
        '%c[NUI Callback]%c ' + url,
        'background: #8b5cf6; color: #fff; padding: 2px 6px; border-radius: 3px;',
        'color: #94a3b8;',
        body
    );
    return originalFetch.apply(this, arguments);
};

使用 Resmon 和 Timing 进行性能分析

超越基础 resmon 监控,你可以在脚本中构建精确的时间测量。测量特定操作所需时间,并在超出可接受阈值时记录警告。这对数据库查询、复杂计算和处理大量实体的循环尤为重要。

-- shared/profiler.lua
local Profiler = {}

function Profiler.Start(label)
    return {
        label = label,
        startTime = GetGameTimer()
    }
end

function Profiler.Stop(timer, warnThresholdMs)
    local elapsed = GetGameTimer() - timer.startTime
    warnThresholdMs = warnThresholdMs or 5

    if elapsed >= warnThresholdMs then
        print(('[^1PERF WARNING^0] %s took %dms (threshold: %dms)'):format(
            timer.label, elapsed, warnThresholdMs
        ))
    elseif GetConvar('myresource_debug', 'false') == 'true' then
        print(('[^2PERF^0] %s completed in %dms'):format(timer.label, elapsed))
    end

    return elapsed
end

-- Usage in a server event
RegisterNetEvent('myresource:heavyOperation', function(data)
    local timer = Profiler.Start('heavyOperation')

    -- ... expensive processing ...
    local result = ProcessLargeDataSet(data)

    Profiler.Stop(timer, 10) -- warn if over 10ms
end)

状态袋调试

状态袋功能强大但有时令人困惑。当状态袋值未按预期更新,通常是因为设置在错误实体上、处理器未捕获正确袋名或复制延迟。构建一个状态袋检查命令,导出指定实体的所有状态。

-- server/debug_statebags.lua
RegisterCommand('debugstate', function(source, args)
    local targetId = tonumber(args[1])
    if not targetId then
        print('Usage: debugstate [playerId]')
        return
    end

    local playerPed = GetPlayerPed(targetId)
    if playerPed == 0 then
        print('Player not found: ' .. targetId)
        return
    end

    local entityState = Player(targetId).state
    print(('[^3STATE BAGS^0] Player %d:'):format(targetId))

    -- Print known state keys (state bags don't have an iterator)
    local keysToCheck = {'job', 'gang', 'duty', 'dead', 'phone', 'inventory'}
    for _, key in ipairs(keysToCheck) do
        local val = entityState[key]
        if val ~= nil then
            print(('  %s = %s (%s)'):format(key, tostring(val), type(val)))
        end
    end
end, true)

基本调试检查清单

  • 检查两个控制台。 始终查看服务器控制台(txAdmin 或终端)和客户端控制台(F8)中的错误。一端的错误通常能解释另一端的异常行为。
  • 验证资源状态。 使用 ensure 重启您的资源并 restart 重启单个资源。查看 resmon 确保资源实际运行。
  • 在干净的环境中测试。 禁用与相同系统交互的其他脚本。许多错误来自资源间冲突,而非单个脚本内的错误。
  • 仔细阅读错误堆栈跟踪。 Lua 堆栈跟踪显示确切的文件和行号。自下而上阅读它们以理解导致错误的调用链。
  • 对高风险操作使用 pcall。 将数据库查询、导出调用和 JSON 解码包装在 pcall 优雅地捕获错误,避免脚本崩溃。
  • 为您的配置版本控制。 当玩家报告 BUG 时,询问其运行的版本。许多问题源于更新后配置文件过时。

准备好开始了吗?

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