Defold 性能分析与调试工具链

系统讲解 Defold 性能分析与调试全家桶:内置 Profiler(CPU/GPU/内存)、Draw Call 统计、Lua 内存泄漏排查、消息流量分析、Web Profiler 远程调试、HTML5 调试、日志级别与断言、VS Code 调试集成。

引言

Defold 游戏上线前,性能瓶颈在哪里?是 Draw Call 太高?Lua GC 卡顿?还是消息风暴拖垮了帧率?没有数据支撑的优化都是「猜」,而 Defold 提供了从内置 Profiler 到 Web 远程调试的完整工具链,让你精准定位问题。本文系统讲解 Defold 性能分析与调试:内置 Profiler(CPU/GPU/内存)怎么看、Draw Call 统计怎么用、Lua 内存泄漏怎么排查、msg.post 消息流量怎么分析、Web Profiler 远程调试怎么配、HTML5 Build 怎么远程连调、日志级别与断言怎么分层、VS Code 怎么集成断点调试。

前置:/defold-performance-optimization/(性能优化策略与手段)。输入与消息系统见 /defold-input-system/。


目录


1. 分析思维:先诊断再动手

优化铁律:先测量,再优化——不量不优化:

1. 定义目标帧率 → 60fps = 16.6ms/帧
2. 用 Profiler 看各模块耗时占比
3. 找出占比最大的「大头」
4. 针对大头做优化
5. 验证优化效果再循环

Defold 性能分析三支柱:

支柱工具解决的问题
CPU内置 Profiler / Web ProfilerLua 脚本/消息/逻辑耗时
GPU/渲染Draw Call 计数 / Render 面板渲染管线瓶颈
内存Lua profiler / 显存面板内存泄漏、GC 压力

记忆:分析 = CPU(逻辑)+ GPU(渲染)+ 内存(泄漏/压力)——先搭好这三维数据,再看哪个维度超标。


2. 内置 Profiler:CPU、GPU 与内存

2.1 如何打开内置 Profiler

平台按键
DesktopF6
HTML5build-html5 后按 F6
移动端需在 game.project → display → vsync 关闭后看

Profiler 面板:

+----------------------------------+
| Frame: 16.2ms | FPS: 61          |
+----------------------------------+
| Script:  5.1ms (31%)             |
| Render:  4.8ms (30%)             |
| Game Object: 2.3ms (14%)         |
| Physics: 1.9ms (12%)             |
| Sound:   0.8ms (5%)              |
| Other:   1.3ms (8%)              |
+----------------------------------+

2.2 Profiler API:代码内采样

-- 手动标记代码段的耗时
function update(self, dt)
    profiler.scope_begin("ai_update")
    update_all_enemies(self)
    profiler.scope_end()

    profiler.scope_begin("collision_check")
    check_collisions(self)
    profiler.scope_end()
end

profiler 模块函数:

local profiler = require("profiler")

profiler.enable_ui(true)   -- 启用/禁用内置 Profiler UI
profiler.set_cpu_profile_interval(0.1)   -- CPU 采样间隔(秒)

-- 内存快照
local mem = profiler.get_memory_usage()
print("Lua 内存:", mem / 1024, "KB")

说明:console 兼容模式下,profiler 模块可能需要引擎版本 >= 1.6.0。低版本用 msg.post("@system:", "toggle_profile") 开启。

2.3 各模块含义

模块说明超标时的应对
ScriptLua 脚本执行、消息处理优化循环、减少 table 创建、对象池
RenderGPU 命令提交、Draw Call合并图集、减少 Sprite/Label 数量
Game ObjectGO 更新、变换计算减少对象数、视锥剔除
Physics2D 物理模拟步进减少刚体/碰撞体、用触发器_sensor
Sound音频解码与播放减少同时播放数、用 OGG

记忆:Profiler 看「谁占大头」,大头在哪就优化哪——别在渲染问题上调 Lua。


3. Draw Call 统计:渲染性能的指挥棒

Draw Call = 每次向 GPU 发送的绘制指令——Defold 的目标是越少越好。

3.1 看 Draw Call

1. 按 F6 打开 Profiler
2. 看 "Render" 行 → 展开后看 "Draw Calls"
3. 目标:同图集精灵尽量合并 → Draw Call ≈ 图层数

Draw Call 来源:

来源                    说明
========================
Sprite                  每个来自不同图集的 Sprite = +1
text/label              每个 Label = +1(动态文字无法合并)
Tilemap                 Tile 层数决定
GUI                     每个 GUI box/node
Particle emitter        每帧独立的粒子系统
Dynamic texture         动态纹理切换 = +1

3.2 用 profiler API 统计 Draw Call

function update(self, dt)
    -- 获取当前帧的 Render 数据(部分版本支持)
    local stats = render.get_stats()
    if stats then
        print("Draw Calls:", stats.draw_calls)
        print("Triangles:", stats.triangles)
    end
end

更直接的 Draw Call 优化验证:

function check_draw_call_reduction(self)
    -- 优化前:
    -- 100 个来自不同图集的精灵 → 100 Draw Call
    -- 优化后:
    -- 100 个来自同一图集的精灵 → 1 Draw Call
    print("将多个小图合并到一个图集(Atlas),可减少 Draw Call")
end

4. Lua 内存泄漏排查:GC 与 profiler

4.1 Lua GC 机制速览

1. 创建 table/string → 分配内存
2. 引用计数归零 → GC 标记回收
3. GC 步进频率可配(collectgarbage("setpause"))
4. 泄漏 = 对象不再使用但被意外引用 → GC 无法回收

4.2 内存监控代码

function update(self, dt)
    self.mem_check_timer = (self.mem_check_timer or 0) + dt
    if self.mem_check_timer >= 5 then
        self.mem_check_timer = 0
        local kb = collectgarbage("count")
        print("Lua 内存:", math.floor(kb), "KB")
        print("Lua 内存:", math.floor(kb/1024), "MB")
    end
end

4.3 泄漏排查技巧

-- 技巧 1:强制 GC 前后对比
function force_gc_and_check()
    local before = collectgarbage("count")
    collectgarbage("collect")
    local after = collectgarbage("count")
    print("回收前:", before, "KB, 回收后:", after, "KB")
    print("本次回收:", math.floor(before - after), "KB")
end

-- 技巧 2:追踪 table 增长
function track_table_growth(name, t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    print("Table [" .. name .. "] 大小:", count)
end

-- 技巧 3:弱引用表检测泄漏
local weak_cache = setmetatable({}, { __mode = "v" })
-- 放入 weak_cache 的对象会在不再需要时被 GC 自动清除
-- 如果对象没有从 weak_cache 消失 → 说明被其他地方强引用

4.4 常见泄漏场景

场景泄漏原因修复
全局注册table.insert(global_list, obj) 后 forget注销时 table.remove
消息回调msg.post 回调中持有引用清理时取消注册
Timer 不清理timer.delay 返回的 handle 未取消timer.cancel(handle)
GO delete 不干净final() 未清理跨对象引用final() 中解绑

5. msg.post 消息流量分析

消息风暴 = 每帧发送海量消息 → Script 模块 profiler 飙升。

5.1 消息流量监控

local msg_counter = 0

-- 包装 msg.post 做计数
function tracked_post(url, message_id, message)
    msg_counter = msg_counter + 1
    msg.post(url, message_id, message)
end

function update(self, dt)
    msg_counter = 0
    -- 业务逻辑结束后
    print("本帧消息数:", msg_counter)
end

5.2 消息优化策略

-- 差量消息:只发变化的数据
function update(self, dt)
    -- 不优:每帧发完整位置
    -- msg.post("/camera", "set_pos", { x = pos.x, y = pos.y })

    -- 优化:只在变化时发
    if vmath.length_sqr(self.last_pos - self.pos) > 1 then
        self.last_pos = self.pos
        msg.post("/camera", "set_pos", self.pos)
    end
end

6. Web Profiler:远程实时性能看板

Web Profiler = 在浏览器中实时查看游戏的 Profiler 数据——对移动端调试极重要。

6.1 启动 Web Profiler

1. 编译游戏(Bundle/Build)
2. 启动游戏后,在终端看到:
   INFO:PROFILER: Profiler started on port 8001
3. 浏览器访问:http://localhost:8001
4. 看到实时帧时间、Draw Call、内存等图表

game.project 配置:

[profiler]
tracking_level = 2      ; 0=关闭, 1=精简, 2=完整
waiting_message = 120   ; 等待 Web Profiler 连接的超时(秒)
port = 8001             ; 默认端口

说明:HTML5 build 时 Web Profiler 通常自动开启,Desktop 需确认 profiler.port 配置。

6.2 Web Profiler 面板解读

+------------------------------------------------+
| Charts: Frame Time | FPS | Draw Calls | Memory |
+------------------------------------------------+
| Modules (按耗时排序):                          |
|   Script: ████████████  12.3ms                 |
|   Render: ██████████    10.1ms                 |
|   Physics: ████          4.2ms                 |
+------------------------------------------------+
| 可点击展开每个模块的详细调用栈                 |
+------------------------------------------------+

远程调试场景:

场景 1:iOS 真机测试
→ 手机连同一 WiFi,访问手机 IP:profiler_port
→ 不用连 Xcode 就能看到性能数据

场景 2:QA 测试收集数据
→ 让 QA 打开 Web Profiler 页面
→ 截图保存 Frame Time 走势图
→ 开发按需优化

7. HTML5 Build 远程调试

Defold 的 HTML5 构建基于 WebGL + WASM,可以用浏览器 DevTools 直接调试。

7.1 打包 HTML5

Project → Bundle → HTML5 Application
→ 输出目录包含 index.html + 游戏数据
→ 用 `python -m http.server` 或 `npx serve` 本地启动

7.2 Chrome DevTools 调试 Lua

1. F12 打开 DevTools
2. Sources 标签页 → 找到 `*.lua` 文件
3. 在 Lua 脚本行号旁点击设置断点
4. 触发对应逻辑 → 断点暂停,可查看变量

Defold HTML5 构建已内置 Source Map,Lua 源码可直接映射——如果版本较老可能需要手动构建。

7.3 控制台查看日志

-- Lua 中 print → 浏览器 console
function update(self, dt)
    print("Frame delta:", dt)
    pprint(self)   -- 漂亮打印 table
end

8. 日志、断言与 VS Code 调试集成

8.1 日志级别控制

-- 自定义日志级别
local LOG_LEVEL = { DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4 }
local current_level = LOG_LEVEL.DEBUG

function log(level, msg, ...)
    if level >= current_level then
        print(string.format(msg, ...))
    end
end

-- 使用
log(LOG_LEVEL.DEBUG, "Player pos: %s", tostring(self.pos))
log(LOG_LEVEL.ERROR, "Failed to load level: %s", err)

8.2 断言

function assert_not_nil(value, name)
    if value == nil then
        error("Assertion failed: " .. name .. " is nil")
    end
end

-- 使用
function init(self)
    assert_not_nil(self.hp, "self.hp")
    assert_not_nil(self.speed, "self.speed")
end

8.3 开发/发布模式切换

; game.project — 开发模式
[project]
developer_mode = 1

[bootstrap]
debug_logging = 1

8.4 VS Code + Lua 调试(插件配置)

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lua",
            "request": "attach",
            "name": "Attach to Defold",
            "host": "localhost",
            "port": 5678
        }
    ]
}

注意:VS Code 原生 Lua 调试对 Defold 的支持较有限。推荐用内置 Profiler + Web Profiler 做性能分析,用 DevTools 做 HTML5 断点调试。


9. 速查表

需求方法关键指标
打开内置 ProfilerF6各模块耗时占比
CPU 瓶颈定位Profiler Script 行帧时间 > 16.6ms
Draw Call 统计Profiler Render 行目标 < 100
Lua 内存collectgarbage("count")持续增长 = 泄漏
强制 GCcollectgarbage("collect")回收前后对比
消息风暴包装 msg.post 计数每帧消息数
Web Profiler浏览器访问 port:8001实时图表
HTML5 调试Chrome DevTools Sources断点 + Console
日志分层自定义 LOG_LEVEL 宏DEBUG/INFO/ERROR
对象泄漏弱引用表 + 手动 GC弱表内对象不消失

一句话记忆:Defold 分析工具链 = 内置 Profiler 看模块占比(F6)→ Script 调 Lua、Render 看 Draw Call、Physics 看碰撞体;Lua 内存靠 collectgarbage 监控 + 弱表查泄漏;消息靠包装计数防风暴;Web Profiler 远程看实时;HTML5 用 DevTools 断点;日志分层 + 断言兜底——先数据后优化,不猜不蒙。


相关阅读

  • /defold-performance-optimization/ — 性能优化策略与实践
  • /defold-script-system-lua/ — Lua 运行时机理与 GC
  • /defold-input-system/ — 消息系统与事件分发
  • /defold-cross-platform-publish/ — HTML5 打包与发布

延伸阅读

  • /defold-shader-postprocessing/ — 渲染管线性能权衡
  • /defold-physics-collision/ — 物理性能优化
  • /defold-save-serialization/ — 序列化内存开销
  • [[game]] — 游戏引擎性能分析理论
  • [[graphics]] — GPU 渲染管线
-- ======================================
-- 完整可运行示例:Profiler + 调试 + 内存监控
-- 放入 debug_manager.script
-- ======================================

function init(self)
    -- 初始化统计
    self.frame_count = 0
    self.mem_samples = {}
    self.msg_count = 0
    self.last_mem = collectgarbage("count")

    -- 启用内置 Profiler UI
    local ok, profiler = pcall(require, "profiler")
    if ok then
        profiler.enable_ui(true)
        print("[init] Profiler UI 已启用")
    else
        print("[init] profiler 模块不可用")
    end

    print("[init] Debug Manager 初始化完成")
end

function update(self, dt)
    self.frame_count = self.frame_count + 1
    self.msg_count = 0   -- 每帧重置消息计数

    -- 每 60 帧采样一次内存(约 1 秒 @60fps)
    if self.frame_count % 60 == 0 then
        local mem_kb = collectgarbage("count")
        local diff = mem_kb - self.last_mem
        self.last_mem = mem_kb

        table.insert(self.mem_samples, mem_kb)
        if #self.mem_samples > 20 then
            table.remove(self.mem_samples, 1)
        end

        print(string.format(
            "[mem] Lua: %.1f KB | 变化: %+.1f KB | 帧: %d",
            mem_kb, diff, self.frame_count
        ))

        -- 如果内存持续增长超过阈值,报警
        if #self.mem_samples >= 10 then
            local trend = self.mem_samples[#self.mem_samples] - self.mem_samples[1]
            if trend > 500 then
                print("[WARN] 内存持续增长! 趋势:", trend, "KB/10s")
            end
        end
    end
end

-- 包装 msg.post 做消息计数
function tracked_post(self, url, message_id, message)
    self.msg_count = self.msg_count + 1
    msg.post(url, message_id, message)
end

function on_message(self, message_id, message, sender)
    if message_id == hash("track_post") then
        tracked_post(self, message.url, message.id, message.payload)
    elseif message_id == hash("force_gc") then
        local before = collectgarbage("count")
        collectgarbage("collect")
        local after = collectgarbage("count")
        print(string.format(
            "[gc] 回收前: %.1f KB, 回收后: %.1f KB, 释放: %.1f KB",
            before, after, before - after
        ))
    elseif message_id == hash("dump_stats") then
        print("=" .. string.rep("=", 40))
        print("[dump] 运行统计:")
        print("  总帧数:", self.frame_count)
        print("  消息量:", self.msg_count, "/ 当前帧")
        print("  当前 Lua 内存:", math.floor(collectgarbage("count")), "KB")
        print("=" .. string.rep("=", 40))
    end
end

function final(self)
    print("[final] Debug Manager 退出,总帧数:", self.frame_count)
end

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold Tilemap 碰撞与关卡设计
  2. Defold Collections、Factories 与动态实例化
  3. Defold IAP 与广告接入:内购流程、Store 验证与广告变现