引言
Defold 以「轻量高效」著称,但不优化照样卡。性能优化的核心是 Draw Call(渲染指令数)——它决定 GPU 的吞吐。本文系统讲 Defold 性能优化:先从「瓶颈在哪」的诊断讲起(用 Profiler),再逐个击破最大头——Draw Call 与图集批处理、纹理与内存、Sprite/粒子/物理对象数量、Lua 脚本开销(GC/表/循环),最后覆盖包体与加载优化,让你把 Defold 游戏从「能跑」优化到「流畅跑满帧」。
前置:/defold-editor-asset-pipeline/(资源与图集)、/defold-script-system-lua/(Lua 运行时)。渲染原理见 [[graphics]]。
目录
- 1. 性能优化的起点:先诊断再优化
- 2. Draw Call:渲染性能的头号指标
- 3. 图集与批处理:合并精灵减少指令
- 4. 纹理与内存:显存怎么省
- 5. 对象数量:Sprite、粒子与物理
- 6. Lua 优化:GC、表与循环
- 7. 物理性能:碰撞体与刚体
- 8. Profiler 实战:定位瓶颈
- 9. 包体与加载优化
- 10. 速查表
- 延伸阅读
1. 性能优化的起点:先诊断再优化
优化铁律:先测量,再动手——不量不优化:
1. 定义目标帧率(60fps = 16.6ms/帧)
2. 用 Profiler 看每帧时间都花在哪
3. 找「大头」优化,别抠芝麻
4. 优化后再量,验证是否真提升
Defold 的 Profiler:
菜单 View → Profiler(或 Build 带 Profiler 的 debug 包)
显示:Script、Render、Physics、Sound 各占多少
瓶颈分布直觉:
| 瓶颈 | 特征 | 大头来源 |
|---|---|---|
| CPU(Script) | update 耗时长 | Lua 循环/GC |
| Render | Draw Call 高 | 精灵过多未合并 |
| Physics | 物理时间高 | 碰撞体过多 |
| GPU | 填充率高 | 特效/大屏闪烁 |
心智:优化 = 用数据找「最贵的那一块」——别凭感觉优化,Profiler 说话。
2. Draw Call:渲染性能的头号指标
Draw Call = GPU 的一次绘制指令——数量决定渲染吞吐:
每帧渲染每个精灵 = 一次 Draw Call
1000 个独立精灵 = 1000 Draw Call → 卡
同样精灵分批合并 → 几十次 → 流畅
影响 Draw Call 的因素:
| 因素 | 影响 |
|---|---|
| 精灵数量 | 每个精灵一次调用 |
| 纹理切换 | 换图集 = 新调用 |
| 材质 | 不同材质分开 |
| Blend 模式 | 透明/不透明分开 |
为什么 Draw Call 重要:GPU 每指令有固定开销——指令数上去了,再快的 GPU 也白搭。
记忆:Draw Call 是渲染的头号瓶颈——目标是把「上千次」压到「几十次」,手段就是图集合并。
3. 图集与批处理:合并精灵减少指令
图集合并 = 让多个精灵「一次绘制」:
多个精灵都在「同一张图集 + 同一材质」→ 引擎自动合批(batch)
跨图集 / 跨材质 → 拆成多个 Draw Call
图集优化实践:
1. 同类精灵放同一个图集(角色/场景/粒子分开)
2. 图集别太大(2048 内,超了伤内存)
3. 帧动画帧放同一图集(批处理生效)
4. 动态图集 vs 静态:静态尽量合并
减少 Draw Call 的其他手段:
| 手段 | 做法 |
|---|---|
| 少用大范围独立透明精灵 | 合成纹理 |
| 粒子的图集 | 同图粒子合批 |
| 缓存静态场景 | 合并成单图 |
| 控制相机外渲染 | 视锥剔除 |
记忆:图集是批处理的钥匙——同图集同材质自动合批,跨图集就拆,这是减 Draw Call 的第一招。
4. 纹理与内存:显存怎么省
纹理内存 = 宽 × 高 × 字节每像素 × 层:
2048×2048 RGBA(4字节)= 16MB
1024×1024 RGBA = 4MB
像素格式决定内存:RGBA > RGB > 压缩格式
省显存手段:
| 手段 | 说明 |
|---|---|
| 压缩纹理 | ASTC/ETC2(移动端) |
| 降分辨率 | 非关键图缩小 |
| 图集精简 | 去掉空白/重复 |
| 卸载资源 | 场景切走时 unload |
| 复用图集 | 同类共用 |
Defold 资源管理:
-- 动态卸载不再用的图集
resource.unload("atlas:level2")
-- 查看内存占用
print("texture mem:", resource.get_texture_memory())
记忆:显存 = 图集大小 × 格式——压缩纹理 + 控制分辨率 + 用完卸载,三管齐下压内存。
5. 对象数量:Sprite、粒子与物理
对象数量对 CPU 的压力:
每个 Sprite:一个 game object 组件 → CPU 更新开销
每个粒子:每帧算位置
每个刚体:物理模拟开销
数量优化策略:
| 对象 | 优化 |
|---|---|
| Sprite | 超出视野停更/隐藏;静态合并 |
| 粒子 | 减少粒子数、限制发射率 |
| 游戏对象 | 复用池(不频繁 create/destroy) |
| 相机外 | 视锥剔除(引擎自带) |
对象池(复用)——最常用的省创建开销技巧:
-- 简易对象池
local pool = {}
local function spawn(self, prototype)
local obj = table.remove(pool) or factory.create(prototype)
return obj
end
local function despawn(self, obj)
table.insert(pool, obj) -- 回收复用
msg.post(obj, "disable") -- 隐藏而非销毁
end
记忆:对象数量是 CPU 大头——用对象池复用、隐藏替代销毁、视锥剔除看不见的——别频繁 create/destroy。
6. Lua 优化:GC、表与循环
Lua 的性能杀手:GC(垃圾回收)与表分配:
每帧 new 一堆表/字符串 → GC 频繁 → 卡顿(卡顿多来自 GC)
循环遍历大表 → 每帧开销累积
GC 优化手段:
-- 1. 复用表,别每帧新建
local tmp = {} -- 初始化一次
function update(self, dt)
table.insert(tmp, 1) -- 复用 tmp 做临时运算
table.clear(tmp) -- 用完清空复用
end
-- 2. 局部变量缓存(避免全局查找)
local math_floor = math.floor -- 局部化热点函数
-- 3. 减少字符串拼接(用 table.concat)
local parts = { "a", "b", self.name }
local s = table.concat(parts) -- 别用 .. 拼大串
循环优化:
-- 缓存 # 长度,避免每轮重算
local n = #list
for i = 1, n do
-- 使用 list[i]
end
| 优化 | 收益 |
|---|---|
| 复用表 | 减 GC |
| 局部化 | 提速查找 |
| table.concat | 减字符串分配 |
| 缓存长度 | 提速循环 |
记忆:Lua 优化 = 少分配(复用表)、少 GC、局部化热点——最贵的不是计算,是内存分配。
7. 物理性能:碰撞体与刚体
物理引擎开销来源:
每帧:碰撞检测 + 求解接触
刚体数量 × 碰撞复杂度 → 时间
物理优化手段:
1. 静态物体用静态碰撞体(不参与动态模拟)
2. 传感器(sensor)别用刚体(只检测不响应)
3. 减少高精度碰撞形状(凸包 > 精细网格)
4. 碰撞层过滤:不相干的层不检测
5. 休眠:不动刚体自动休眠(引擎支持)
碰撞层过滤(物理层):
-- 物理属性里设置碰撞层
-- 玩家只与「ground」和「enemy」碰撞,别与「particle」碰撞
physics.set_filter("#collider", {
mask_bits = { ground = 1, enemy = 1 },
group = "player",
})
记忆:物理优化四招——静态别做动态、传感器别上刚体、碰撞层过滤、让刚体休眠——物理时间是省得最快的一块。
8. Profiler 实战:定位瓶颈
用 Profiler 定位瓶颈的完整流程:
1. Build debug 包(带 Profiler)
2. 打开 View → Profiler
3. 复现卡顿场景(波次/大量敌人)
4. 看各模块占比:Script / Render / Physics / Sound
5. 定位大头 → 针对性优化
各模块占比怎么读:
| 模块占比高 | 优化方向 |
|---|---|
| Script | Lua/GC/对象数量 |
| Render | Draw Call/纹理 |
| Physics | 碰撞体/刚体 |
| Sound | 音频组件数/格式 |
| Game Object | 对象创建销毁 |
关键指标:
Frame 时间(目标 <16.6ms @60fps)
Draw Calls 数
GC 暂停次数
Physics 步进时间
记忆:Profiler 是「分模块计时器」——先看谁占大头,别在渲染问题上调 Lua。
9. 包体与加载优化
包体(下载体积)优化:
| 手段 | 效果 |
|---|---|
| 压缩纹理 | 显著减体积 |
| 图集去冗余 | 减纹理 |
| OGG 音乐 | 比 WAV 小 10 倍 |
| 资源瘦身 | 删未用资源 |
| 平台分包 | 按平台出包 |
加载优化:
1. 懒加载:场景切到时才加载
2. 预加载:转场前预取关键资源
3. 热更新:大资源走 Live Update(见 /defold-hot-reload-updates/)
4. 进度条:加载 UI 反馈
Defold 加载 API:
-- 预加载资源
resource.load("atlas:boss", function() start_boss() end)
-- 或按需 create factory
记忆:包体靠压缩纹理 + OGG + 删冗余;加载靠懒加载 + 预加载 + 热更——下载小、进图快,体验就好。
10. 速查表
| 需求 | 做法 |
|---|---|
| 找瓶颈 | Profiler 看各模块占比 |
| 减 Draw Call | 同类精灵同一图集 |
| 省显存 | 压缩纹理 + 降分辨率 |
| 对象复用 | 对象池(隐藏非销毁) |
| 减 GC | 复用表 + table.concat |
| 物理省时 | 静态碰撞体 + 层过滤 |
| 防卡顿 | 视锥剔除 + 对象池 |
| 减包体 | 压缩纹理 + OGG |
| 加快加载 | 懒加载 + 预加载 + 热更 |
| 验证 | 优化后再跑 Profiler |
一句话记忆:Defold 优化先量后改——Profiler 找大头;渲染看 Draw Call、同图集批处理;内存靠压缩纹理 + 卸载;CPU 靠对象池 + 复用表减 GC;物理用静态体 + 层过滤;包体靠压缩 + 懒加载——优化一圈,帧率自然回来。
延伸阅读
- /defold-editor-asset-pipeline/ — 图集与资源管线
- /defold-script-system-lua/ — Lua 运行时优化
- /defold-hot-reload-updates/ — 资源热更新
- /defold-physics-collision/ — 物理性能
- /defold-shader-postprocessing/ — 着色器性能权衡
- [[graphics]] — 渲染管线与图集原理
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。