Unity 性能优化速查

这篇按性能问题来查:卡顿看哪、GC 哪里来、对象池怎么写、UI 和渲染先关心什么。


快速索引

现象 先查
偶发卡一下 GC、Instantiate、加载、Shader 编译
每帧都慢 Update、UI Rebuild、Rendering、Physics
越玩越卡 对象没释放、资源没卸载、列表增长、事件残留
Editor 流畅真机卡 分辨率、后处理、阴影、贴图、平台性能
打开 UI 卡 Layout Rebuild、列表创建、图片加载
子弹/特效多时卡 对象池、粒子数量、碰撞检测
内存高 贴图、音频、网格、Addressables 释放

0. Profiler 看哪里

1
2
3
4
5
6
CPU Usage              # 脚本、物理、动画、UI 耗时
GC Alloc # 每帧托管内存分配
Rendering # Draw Call、SetPass、渲染耗时
Memory # 贴图、网格、音频、托管堆
UI # Canvas Rebuild、Layout Rebuild
Physics # 物理模拟、碰撞、射线检测

建议流程:

1
2
3
4
5
目标平台复现
不开 Deep Profile 看整体
定位模块后短时间开 Deep Profile
只改一个点
优化前后对比数据

1. GC 常见来源

1
2
3
4
5
6
7
8
Update 里 new List / new Dictionary
Update 里 LINQ
字符串频繁拼接
foreach 某些集合枚举
装箱 boxing
闭包 lambda
Debug.Log 高频输出
ToString 高频调用

减少 GC:

1
2
3
4
5
6
private readonly List<Enemy> results = new(); // 复用列表

private void Update()
{
results.Clear(); // 清空但不重新分配
}

字符串减少刷新:

1
2
3
4
5
if (lastHp != currentHp)
{
hpText.text = currentHp.ToString(); // 变化才更新 UI
lastHp = currentHp;
}

2. 对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public sealed class SimplePool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
private readonly Queue<GameObject> pool = new();

public GameObject Get()
{
GameObject obj = pool.Count > 0
? pool.Dequeue()
: Instantiate(prefab);

obj.SetActive(true); // 取出时激活
return obj;
}

public void Release(GameObject obj)
{
obj.SetActive(false); // 回收时隐藏
pool.Enqueue(obj);
}
}

适合池化:

1
2
3
4
5
6
子弹
特效
伤害数字
UI Item
怪物
音效源

回收时检查:

1
2
3
4
5
6
停止协程
取消事件
清理状态
关闭粒子
关闭碰撞
重置父节点

3. Update 优化

少用:

1
2
3
4
5
private void Update()
{
FindObjectOfType<Player>(); // 每帧查找
GetComponent<Rigidbody>(); // 每帧取组件
}

降频:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private float timer;

private void Update()
{
timer += Time.deltaTime;

if (timer < 0.2f)
{
return;
}

timer = 0f;
ScanEnemies(); // 每 0.2 秒扫一次
}

事件驱动:

1
player.HpChanged += RefreshHp;                // 血量变化才刷新 UI

4. UI 性能

1
2
3
4
5
动态 UI 单独 Canvas                 # 减少整块重建
列表 Item 复用 # 避免频繁创建销毁
装饰图关闭 Raycast Target # 减少射线检测
减少 Layout Group 嵌套 # 降低布局计算
文本变化才赋值 # 避免反复 dirty

查 Profiler:

1
2
3
4
Canvas.SendWillRenderCanvases
Layout.Rebuild
GraphicRaycaster.Raycast
TMP.GenerateText

5. 渲染优化

1
2
3
4
5
Draw Call / Batches                  # 批次数
SetPass Calls # 材质切换
Overdraw # 透明重叠
Shadow # 阴影开销
Post Processing # 后处理开销

材质实例化坑:

1
renderer.material.color = Color.red;          // 会生成材质实例

批量改共享材质:

1
renderer.sharedMaterial.color = Color.red;    // 改共享材质,影响所有使用者

单对象改参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
private MaterialPropertyBlock block;

private void Awake()
{
block = new MaterialPropertyBlock();
}

private void SetColor(Color color)
{
renderer.GetPropertyBlock(block);
block.SetColor("_BaseColor", color);
renderer.SetPropertyBlock(block); // 不复制材质
}

6. 物理优化

1
2
3
4
5
Layer Collision Matrix               # 关闭不需要的层碰撞
Fixed Timestep # 物理步长别过高
Raycast 加 LayerMask # 缩小检测范围
NonAlloc API # 减少分配
Collider 数量 # 控制碰撞体复杂度

Raycast:

1
2
3
4
if (Physics.Raycast(origin, direction, out RaycastHit hit, 20f, enemyMask))
{
Debug.Log(hit.collider.name); // 只检测 enemyMask
}

NonAlloc:

1
2
3
private readonly RaycastHit[] hits = new RaycastHit[16];

int count = Physics.RaycastNonAlloc(ray, hits, 20f, enemyMask); // 不分配数组

7. 资源导入

贴图:

1
2
3
4
5
Max Size 是否过大
Compression 是否按平台设置
Read/Write 是否关闭
Mip Map 是否需要
Alpha 是否真的需要

模型:

1
2
3
4
Read/Write 是否关闭
Rig / Animation 是否需要
Mesh Compression 是否可接受
不需要的节点和动画是否删除

音频:

1
2
3
短音效:Decompress On Load
长音乐:Streaming
压缩格式按平台设置

8. 移动端检查

1
Application.targetFrameRate = 60;             // 设置目标帧率
1
2
3
4
5
6
7
真机测试,不只看 Editor
发热后是否降频
分辨率 / Render Scale 是否过高
后处理是否过重
阴影距离是否过大
透明 UI 是否过多
包体和内存是否超预算

9. 常见坑速查

1
2
3
4
5
6
7
没看 Profiler 凭感觉优化              # 容易改错方向
Update 每帧分配 # GC 抖动
大量 Instantiate / Destroy # 峰值卡顿
UI 列表全销毁重建 # 打开界面卡
访问 renderer.material # 材质实例增加
Addressables 不 Release # 内存增长
只测 Editor 不测真机 # 结果不准

系列导航