Unity 代码编写规范与优化标准 - 从代码质量到性能调优

涵盖代码优化标准、内存管理、渲染优化、AssetBundle 策略、项目发布及 FPS 标准的完整开发规范。


一、优化标准概述

1.1 行业标准理念

1
2
3
4
5
6
7
8
9
10
11
12
优化标准体系:
┌─────────────────────────────────────┐
│ │
│ 不同游戏有不同标准 │
│ ↓ │
│ 但存在行业内基准 │
│ ↓ │
│ 可在合理范围内上下浮动 │
│ ↓ │
│ 判断游戏水平: 差 / 中 / 好 │
│ │
└─────────────────────────────────────┘

1.2 标准分类

标准类型 关注点 重要性
代码优化 GC Alloc、函数调用开销 ⭐⭐⭐⭐⭐
内存优化 堆内存、资源内存、内存泄露 ⭐⭐⭐⭐⭐
渲染优化 DrawCall、Triangle、VBO ⭐⭐⭐⭐
FPS 标准 帧率稳定性、设备适配 ⭐⭐⭐⭐⭐

二、代码优化标准

2.1 Lambda 表达式

规范 说明 风险
尽量避免 减少使用 Lambda 表达式 内存泄露隐患
1
2
3
4
5
6
7
8
9
10
11
12
// ❌ Lambda 可能产生闭包和内存泄露
button.onClick.AddListener(() => {
Debug.Log("Clicked");
});

// ✅ 使用独立方法
button.onClick.AddListener(OnButtonClick);

void OnButtonClick()
{
Debug.Log("Clicked");
}

2.2 LINQ 查询

规范 说明 影响
尽量避免 减少使用 LINQ 大量 GC Alloc
1
2
3
4
5
6
7
8
9
10
// ❌ LINQ 产生大量 GC
var activeEnemies = enemies.Where(e => e.isActive).ToList();

// ✅ 使用传统循环
List<Enemy> activeEnemies = new List<Enemy>();
foreach (var enemy in enemies)
{
if (enemy.isActive)
activeEnemies.Add(enemy);
}

2.3 协程控制

规范 说明 内存开销
控制使用次数 减少 StartCoroutine 调用 每次至少 37B
Coroutine 类 21B -
Enumerator 16B -

优化方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ❌ 频繁开启协程
void Start()
{
StartCoroutine(DoSomething()); // 每次至少 37B
}

// ✅ 方案 1: 使用全局协程
private IEnumerator globalCoroutine;

void Start()
{
globalCoroutine = GlobalCoroutineManager();
StartCoroutine(globalCoroutine);
}

IEnumerator GlobalCoroutineManager()
{
while (true)
{
// 统一管理所有延时操作
yield return new WaitForSeconds(1f);
}
}

// ✅ 方案 2: 少用/不用协程
private float timer;
void Update()
{
timer += Time.deltaTime;
if (timer >= 1f)
{
DoSomething();
timer = 0;
}
}

2.4 字符串拼接

方式 GC 影响 推荐场景
String + String 频繁申请释放内存 ❌ 不推荐
StringBuilder 同一内存块操作 ✅ 推荐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 频繁内存分配
string result = "";
for (int i = 0; i < 100; i++)
{
result += i.ToString(); // 每次分配新内存
}

// ✅ 使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
sb.Append(i); // 同一内存块
}
string result = sb.ToString();

2.5 组件缓存

规范 说明 GC 影响
提前缓存 Start 中缓存组件 避免 GetComponent
避免获取名称 不使用 Object.name 分配 39B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 每帧获取组件
void Update()
{
var renderer = GetComponent<Renderer>();
var name = gameObject.name; // 39B GC
}

// ✅ 缓存组件
private Renderer cachedRenderer;
private string cachedName;

void Start()
{
cachedRenderer = GetComponent<Renderer>();
cachedName = gameObject.name;
}

void Update()
{
if (cachedRenderer != null)
{
// 使用缓存
}
}

2.6 避免开销大的函数

函数 开销级别 替代方案
GameObject.Find ⭐⭐⭐⭐⭐ 缓存引用
GameObject.FindWithTag ⭐⭐⭐⭐ 缓存引用
Object.FindObjectOfType ⭐⭐⭐⭐⭐ 单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 每帧查找
void Update()
{
var player = GameObject.Find("Player");
}

// ✅ 使用缓存或单例
private GameObject player;

void Start()
{
player = GameObject.Find("Player");
}

// ✅ 更好: 使用单例
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }

void Awake()
{
Instance = this;
}
}

2.7 标签比较

方式 GC 分配 推荐
gameObject.tag 180B
CompareTag() 0B
1
2
3
4
5
6
7
8
9
// ❌ 产生 180B GC
if (gameObject.tag == "Enemy")
{
}

// ✅ 不产生 GC
if (gameObject.CompareTag("Enemy"))
{
}

2.8 对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ✅ 对象池实现
public class BulletPool : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private Queue<Bullet> pool = new Queue<Bullet>();
public int maxSize = 50;

public Bullet Get()
{
if (pool.Count > 0)
{
var bullet = pool.Dequeue();
bullet.gameObject.SetActive(true);
return bullet;
}
return Instantiate(bulletPrefab);
}

public void Return(Bullet bullet)
{
if (pool.Count < maxSize)
{
bullet.gameObject.SetActive(false);
pool.Enqueue(bullet);
}
else
{
Destroy(bullet.gameObject);
}
}
}

2.9 Update 优化

规范 说明
避免复杂计算 移至协程或定时执行
隔帧计算 降低更新频率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 每帧复杂计算
void Update()
{
ComplexCalculation();
}

// ✅ 隔 N 帧计算
void Update()
{
if (Time.frameCount % 6 == 0) // 每 6 帧执行
{
ComplexCalculation();
}
}

// ✅ 使用协程
IEnumerator Start()
{
while (true)
{
ComplexCalculation();
yield return new WaitForSeconds(0.5f);
}
}

2.10 函数调用优化

规范 说明
避免递归 减少函数调用栈
避免无限循环 仅在核心功能使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 递归调用
int Factorial(int n)
{
if (n <= 1) return 1;
return n * Factorial(n - 1); // 递归深度过大
}

// ✅ 迭代实现
int Factorial(int n)
{
int result = 1;
for (int i = 2; i <= n; i++)
result *= i;
return result;
}

// ❌ 避免的写法
x = Mathf.Abs(x); // 函数调用

// ✅ 优化写法
x = (x > 0 ? x : -x); // 三元运算符

三、纹理优化

3.1 压缩格式

平台 推荐格式 说明
iOS PVRTC Apple 专属格式
Android ETC / ETC2 通用格式
Windows DXT / BC DirectX 格式

3.2 MipMaps 设置

场景 MipMaps 说明
3D 场景 ✅ 启用 提升远距离渲染质量
UI / 2D ❌ 禁用 避免内存浪费
1
2
3
4
5
6
7
8
MipMaps 优化策略:
┌─────────────────────────────────────┐
│ 硬件 MipMaps 不满足需求: │
│ │
│ 1. 使用自定义生成的 Mipmaps │
│ 2. 启用 Anisotropic Filtering │
│ │
└─────────────────────────────────────┘

3.3 纹理大小优化

方法 效果
减色 降低纹理大小
调整尺寸 根据实际显示需求

四、内存优化

4.1 内存目标值

指标 目标值 说明
总内存 <150MB 移动端标准
堆内存 <40MB Mono 堆内存

4.2 场景切换优化

1
2
3
4
5
6
7
8
9
10
11
12
场景切换内存管理:
┌─────────────────────────────────────┐
│ 问题: 两个场景内存叠加 → 峰值 │
│ │
│ 解决方案: │
│ 1. 显示 Loading 遮罩 │
│ 2. 等待旧场景释放完成 │
│ 3. 加载新场景 │
│ 4. 新场景初始化完成 │
│ 5. 隐藏 Loading 遮罩 │
│ │
└─────────────────────────────────────┘

4.3 内存控制策略

策略 说明
使用 Prefab 场景物体应制作成 Prefab
对象池 避免频繁 Instance/Destroy
公共 UI 不释放 切换场景时保留
顺序加载 LoadManager 控制同时加载一个 WWW

4.4 资源内存限制

资源类型 峰值限制
纹理 <50MB
网格 <20MB
动画片段 <15MB
音频 <15MB

4.5 内存泄露检测

1
2
3
4
5
6
7
8
9
10
11
内存健康标准:
┌─────────────────────────────────────┐
│ ✅ 内存升降一致 │
│ ❌ 持续上升(内存泄露) │
│ │
│ 检测方法: │
│ - 反复进入/退出场景 │
│ - 观察 Profiler 内存曲线 │
│ - 确保内存回归基准线 │
│ │
└─────────────────────────────────────┘

五、渲染优化

5.1 渲染目标值

指标 目标值 说明
DrawCall 峰值 <250 主体范围 0-200
Triangle 峰值 <100,000 每帧面数
VBO 上传量 <5MB 使用缓存池
Skinned Mesh <50 蒙皮网格数量
RenderTexture <10 渲染纹理数量

5.2 DrawCall 优化

1
2
3
4
// DrawCall 优化要点
// 1. 相同材质合并
// 2. 动态/静态合批
// 3. 减少 Material Instance

六、CPU 优化

6.1 物理优化

指标 峰值 Profiler 对应
Rigidbody <50/帧 Physics.Simulate
碰撞体 <100/帧 Physics.Simulate

七、AssetBundle 策略

7.1 依赖性打包

1
2
3
4
// 获取资源依赖
string[] dependencies = AssetDatabase.GetDependencies("Assets/Prefabs/Player.prefab", false);

// Push/Pop 管理依赖关系

7.2 WWW vs CreateFromFile

方式 适用场景 压缩格式
WWW 远程加载 Unity 标准压缩
CreateFromFile 本地加载 非压缩格式
1
2
3
4
5
6
7
8
9
10
// WWW 加载标准压缩 AB
IEnumerator LoadFromRemote(string url)
{
WWW www = new WWW(url);
yield return www;
AssetBundle bundle = www.assetBundle;
}

// CreateFromFile 加载本地非压缩 AB
AssetBundle bundle = AssetBundle.CreateFromFile(path);

7.3 AssetBundle.mainAsset

用途 说明
标准资源 存储 AB 中最重要的资源
TextAsset 记录 AB 内物体列表和信息描述
1
2
3
4
5
// 使用 mainAsset 获取索引信息
AssetBundle bundle = AssetBundle.LoadFromFile(path);
TextAsset manifest = bundle.mainAsset as TextAsset;

// 解析 manifest 获取资源列表

7.4 Build AssetBundles

1
2
3
4
5
6
7
8
9
10
11
开发 vs 发布策略:
┌─────────────────────────────────────┐
│ 开发阶段: │
│ → Resources.LoadAssetAtPath │
│ → 避免反复 Build AB │
│ │
│ 发布阶段: │
│ → BuildPipeline.BuildAssetBundle │
│ → 唯一脚本打包 │
│ │
└─────────────────────────────────────┘

7.5 AssetDatabase.ImportAsset

事实 说明
1 转换外部 asset 为内部识别格式
2 根据 AssetImporter 修改属性
3 Build 前强制重新导入
1
2
3
4
// 常用 AssetImporter 类型
- TextureImporter
- ModelImporter
- MovieImporter

7.6 预处理和后处理

AssetPostprocessor 作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Asset 导入流程:
┌─────────────────────────────────────┐
│ │
│ Import 开始 │
│ ↓ │
│ OnPreprocessXXX ← 用户介入修改 │
│ ↓ │
│ Unity 导入处理 │
│ ↓ │
│ OnPostprocessXXX ← 用户介入修改 │
│ ↓ │
│ Import 完成 │
│ │
└─────────────────────────────────────┘

八、项目发布策略

8.1 移动芯片支持

Unity 版本 ARM V6 ARM V7
3.5.x ✅ 支持 ✅ 支持
4.0+ ❌ 不支持 ✅ 支持

8.2 Graphics Emulation

用途 说明
模拟测试 Editor 中模拟不同图形硬件
设置路径 Editor → Graphics Emulation

8.3 代码保护

平台 保护方式 安全性
iOS 原生编译 ✅ 高
Android AssetBundle + TextAsset / 代码混淆 ⚠️ 中
通用 Native DLL ✅ 高
1
2
3
// AssetBundle 代码加密
// 将编译好的 dll 作为 TextAsset 打包
// 参考: http://docs.unity3d.com/Documentation/Manual/scriptsinassetbundles.html

8.4 增量更新方案

方案 优点 缺点
脚本与资源全分离 完全动态 项目管理复杂,无法编辑器动态编辑
接口与实现分离 工作流不变,可编辑 需要额外架构设计
1
2
3
4
5
6
7
8
9
10
11
增量更新原理:
┌─────────────────────────────────────┐
│ 编译后的脚本无法修改 │
│ ↓ │
│ 但可以添加新脚本 │
│ ↓ │
│ 接口不变,实现可替换 │
│ ↓ │
│ 实现 AB 热更新 │
│ │
└─────────────────────────────────────┘

九、FPS 帧率标准

9.1 目标值

设备类型 目标帧率 最低可接受
高端机 60 FPS >50 FPS
中端机 45 FPS >30 FPS
低端机 30 FPS ≥24 FPS

9.2 低端机标准

参考设备 CPU 型号
红米 Note 4 Helio X20

9.3 优化阈值

1
2
3
4
5
6
7
8
FPS 优化决策:
┌─────────────────────────────────────┐
│ FPS ≥ 50: 优秀 │
│ FPS ≥ 30: 可接受 │
│ FPS < 24: ❌ 必须优化 │
│ FPS < 20: ❌❌ 严重性能问题 │
│ │
└─────────────────────────────────────┘

十、UWA 内存优化

10.1 四种测试模式

模式 名称 用途
模式 1 Overview 整体性能分析
模式 2 Mono 堆内存分析(IL2CPP 也需测试)
模式 3 Assets 资源检测,重复/耗费
模式 4 Lua Lua 内存分析(收费)

10.2 测试建议

1
2
3
4
5
6
7
8
9
UWA 测试流程:
┌─────────────────────────────────────┐
│ 1. Overview 模式 → 整体评估 │
│ 2. Mono 模式 → 堆内存分析 │
│ (即使 IL2CPP 也需测试) │
│ 3. Assets 模式 → 资源优化 │
│ 4. Lua 模式 → Lua 优化(可选) │
│ │
└─────────────────────────────────────┘

十一、UPR 内存优化

11.1 UPR 优势

特性 说明
官方支持 Unity 官方性能报告工具
免费使用 无需付费
深度分析 覆盖多维度性能指标

十二、优化检查清单

12.1 代码优化

  • 避免 Lambda 表达式
  • 避免 LINQ 查询
  • 控制 StartCoroutine 次数
  • 使用 StringBuilder 拼接字符串
  • 缓存组件引用
  • 避免使用 Object.name
  • 避免 GameObject.Find
  • 使用 CompareTag 替代 tag 比较
  • 使用对象池
  • 优化 Update 函数
  • 避免递归调用
  • 减少函数调用栈

12.2 内存优化

  • 总内存 <150MB
  • 堆内存 <40MB
  • 场景切换平滑
  • 使用 Prefab
  • 使用对象池
  • 公共 UI 不释放
  • 纹理 <50MB
  • 网格 <20MB
  • 动画 <15MB
  • 音频 <15MB
  • 无内存泄露

12.3 渲染优化

  • DrawCall <250
  • Triangle <100,000
  • VBO 上传 <5MB
  • Skinned Mesh <50
  • RenderTexture <10

12.4 FPS 目标

  • 高端机 60 FPS
  • 中端机 45 FPS
  • 低端机 30 FPS
  • 无低于 24 FPS 情况

十三、参考资料


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com