Unity 游戏开发性能优化全景手册

Unity 游戏开发性能优化全景手册

覆盖:特效 / 模型 / UI / 编程与内存 / 渲染 / 包体 / 开发技巧

定位:面试速查 + 工程落地清单。每个结论都尽量给出原因做法验收指标

面试答题时优先说"我怎么定位、怎么量化、怎么验证",而不是背一堆零散技巧。


第 0 章 优化方法论:先测量,再优化

0.1 三条铁律

  1. 不要凭感觉优化。Editor 里的帧率不等于真机;瓶颈往往和你猜的不一样。
  2. 优化 = 找到瓶颈 + 消除瓶颈 + 验证收益。没有 Profiler 数据的优化都是自嗨。
  3. 优化有代价。合批吃内存、LOD 吃包体、对象池吃常驻内存、降分辨率吃画质。每次优化都要说清"拿什么换了什么"。

0.2 瓶颈的四象限定位

瓶颈类型 典型症状 验证手段
CPU 逻辑 PlayerLoop/脚本函数耗时长,帧时间高但 GPU 空闲 Profiler CPU 面板、Deep Profile 定位具体函数
CPU 渲染提交 Camera.RenderRenderLoop.DrawBatchRenderer 耗时长;DrawCall 数高 Profiler Rendering 面板看 Batches / SetPass Calls
GPU 填充率 / Overdraw 降分辨率后帧率明显回升;GPU ms 强行降 Render Scale 到 0.5 → 若帧率大涨,瓶颈在填充率
GPU 顶点 / 带宽 降低模型面数后帧率回升;纹理大、未压缩 Profiler 看 Tris/Verts、纹理内存
GC 卡顿 每隔几秒一次尖刺(锯齿状帧时间) Profiler CPU 面板的 GC Alloc 列、GC.Collect 标记
内存 闪退、OOM、被系统杀后台 Memory Profiler、Profiler Memory 面板
加载 / IO 切场景、首次播放特效卡顿 Profiler 看 Loading、Shader.CreateGpuProgramTexture.Awake

一行判定法:把分辨率降到 50%,帧率暴涨 → GPU 填充率瓶颈;帧率几乎不变 → CPU 或顶点瓶颈。

0.3 移动端参考预算(中端机,60fps = 16.67ms)

指标 参考值 说明
DrawCall(Batches) 100 ~ 250 超过 300 要重点治理
SetPass Calls < 60 材质切换成本,比 DrawCall 更致命
三角面 / 帧 10 万 ~ 30 万 视机型浮动
GC Alloc / 帧 0 B(理想),< 1KB(可接受) 这是最容易量化也最容易拿分的指标
同屏 SkinnedMesh 10 ~ 30 个 SLG 大量士兵必须走 GPU Skinning / Instancing
纹理内存 < 150 ~ 250 MB 视机型
特效同屏数量 < 20 个系统(不含合批) 配合上限控制

0.4 优化闭环

复制代码
Measure(Profiler 取基线)
   ↓
Locate(定位到具体函数/资源/模块)
   ↓
Fix(改一个点,只改一个)
   ↓
Verify(同一场景、同一机型复测,对比前后数据)
   ↓
Guard(防回归:CI 自动化拍照 + 资源导入规范)

0.5 面试速答模板:方法论

"我优化的第一步永远是测量,不凭感觉。先连真机 Profiler 取基线,分四象限判断瓶颈在 CPU 逻辑、渲染提交、GPU 填充率还是 GC------我常用的快速判定是把 Render Scale 降到 50%,如果帧率暴涨说明瓶颈在填充率,如果几乎不变说明在 CPU 或顶点。然后针对单点改,改完复测对比数据。每帧 GC 分配我会盯到 0,因为这是最容易量化也最容易在低端机放大的指标。优化一定有代价,所以我会同时说明这次拿内存还是画质换了帧率。"


第一章 特效优化

1.1 特效的四个开销来源(按重要性排序)

  1. Overdraw(填充率) ------ 特效的头号杀手。半透明粒子层层叠加,一个像素被反复着色十几次,移动 GPU 直接跪。
  2. 粒子更新计算 ------ CPU 端逐粒子模拟(位置、速度、颜色、大小),粒子越多越贵。
  3. DrawCall 与材质切换 ------ 每个粒子系统至少 1 个 DrawCall,材质不同还会 SetPass。
  4. 内存与加载 ------ 贴图、Shader 变体、预制体常驻;首次播放还要编译 Shader、上传纹理 → 卡顿。

1.2 ParticleSystem 模块级优化

设置项 优化做法 原因
Max Particles 严格设上限,能小就小 直接决定 CPU 模拟量和顶点数
Emission 优先用 Bursts(一次性爆发),少用持续高 Rate over Time 同样视觉冲击力下粒子总数更少
Collision / Triggers 能关就关 最贵的模块,逐粒子做物理查询
Sub Emitters 少用,层级不超过 1~2 层 会指数级增加粒子数
Shape 用简单形状(Cone/Box/Circle),避免复杂 Mesh 发射计算成本
Renderer Mode 优先 BillboardMesh 模式成本高 Mesh 粒子 = 顶点 + 可能的阴影
Cast/Receive Shadows 关闭 阴影 = 再来一遍渲染
Lights 关闭(不用光照粒子) 每粒子光照极贵
Simulation Space 非必要用 Local World 模式每帧要额外变换
Play On Awake / Looping 池化对象手动控制 便于对象池统一管理
未使用的模块 Size over LifetimeRotation by Speed不用的模块整个关掉 每个启用模块都会参与逐粒子计算
Sorting Fudge / Order in Layer 同材质同层级的粒子可合批 减少 DrawCall

重点 :模块关掉比参数调小更有效。一个 Collision 模块的开销可能顶得上几百个粒子。

1.3 粒子材质与贴图

  • 贴图尺寸:粒子贴图 128×128 / 256×256 足够,绝不用 512+。
  • 关闭 Mipmap 、关闭 Read/Write Enabled(否则内存 ×2)。
  • 压缩格式:Android ASTC,iOS ASTC / PVRTC。
  • 合并粒子图集:把常用粒子贴图打到一张 Atlas,多个特效共享一个材质 → 可合批。
  • Shader :用 Mobile 系列(如 Mobile/Particles/Additive)或 URP 的 Universal Render Pipeline/Particles/Unlit
    • 不要给粒子用 PBR/Standard Shader(采样次数多、计算重)。
    • Additive 材质关 ZWrite、关深度测试写入
  • 避免过度使用 Alpha Blend :能做成 Additive 的(火焰、光效)就别用混合,Additive 不需要排序。

1.4 Overdraw 治理(面试高频)

判断方法

  • Unity 场景视图 → Shading ModeOverdraw(越亮/越白说明叠加层数越多,理想是深蓝/黑)。
  • 真机用 RenderDoc / Xcode GPU Frame Capture / Mali Graphics Debugger 看实际填充。

治理手段

  1. 减少粒子体积:同样的覆盖面积,用更少、更大的粒子 vs 很多小粒子 ------ 后者 Overdraw 更分散但总面积更大,需要权衡;通常减小粒子尺寸收益明显。
  2. 减少层数:一个特效从"核心光 + 外圈光 + 火花 + 烟雾 + 拖尾"5 层砍到 3 层。
  3. 缩短生命周期:粒子存活越久,同时在屏的越多。
  4. 避免全屏特效叠加:全屏闪白、全屏泛光不要同屏多个。
  5. 画质分级(见下)。

1.5 特效画质分级(必须有)

csharp 复制代码
// ScriptableObject 配置驱动(架构视角:美术/策划可配,不改代码)
[CreateAssetMenu(menuName = "Config/FXQualitySettings")]
public class FXQualitySettings : ScriptableObject
{
    public float ParticleScale = 1.0f;      // 粒子数量缩放
    public int   MaxSameScreenFX = 20;      // 同屏特效上限
    public bool  EnableSecondaryFX = true;  // 次级特效(击中火花、地面痕迹)
    public bool  EnablePostProcess = true;  // 全屏后处理
}
档位 策略
全量粒子、次级特效开、后处理开
粒子数 ×0.6、次级特效只留主要的、后处理降级
粒子数 ×0.3、关次级特效、关后处理、全屏特效禁用

1.6 特效对象池 + 预加载(架构加分项)

问题 :运行时 Instantiate 粒子预制体 + 首次播放时 Shader 编译 / 纹理上传 → 明显掉帧尖刺。

方案

csharp 复制代码
public class FXPool : MonoBehaviour
{
    [SerializeField] private FXDatabase _database;   // SO: id -> prefab + poolSize
    private readonly Dictionary<int, Queue<ParticleSystem>> _pools = new();

    public ParticleSystem Play(int fxId, Vector3 pos, Quaternion rot)
    {
        if (!_pools.TryGetValue(fxId, out var q)) { q = new Queue<ParticleSystem>(); _pools[fxId] = q; }
        ParticleSystem ps;
        if (q.Count > 0) ps = q.Dequeue();
        else             ps = Instantiate(_database.Get(fxId).Prefab, transform);

        var t = ps.transform;
        t.SetPositionAndRotation(pos, rot);
        ps.gameObject.SetActive(true);
        ps.Play(true);
        StartCoroutine(Recycle(fxId, ps));  // 或用时间片管理器,避免大量协程
        return ps;
    }

    private IEnumerator Recycle(int fxId, ParticleSystem ps)
    {
        yield return new WaitForSeconds(ps.main.duration + ps.main.startLifetime.constantMax);
        ps.Stop(true);
        ps.gameObject.SetActive(false);
        _pools[fxId].Enqueue(ps);
    }
}

关键实践

  • 启动时预热:进游戏前对常用特效各实例化 1 次并播放到不可见处,触发 Shader 编译与纹理上传。
  • ShaderVariantCollection.WarmUp():打包时收集变体,启动时一次性预热,避免运行时编译卡顿。
  • 同屏数量上限:超过上限时按优先级丢弃(主角大招 > 小怪受击)。
  • 距离剔除:远处/屏幕外的特效不播或降级。
  • 回收用统一 Tick 管理,避免每个特效起一个协程(协程本身有分配)。

1.7 其他特效形态

类型 优化点
TrailRenderer 调大 Min Vertex Distance(减少顶点)、设 Time 不要过长、关阴影
LineRenderer 同理,顶点数控制
全屏后处理(Bloom / Radial Blur / 屏幕扭曲) 移动端慎用;降分辨率执行 RT;缩短持续时间;多个合并成一个 Pass
URP Render Feature 不用的一律关掉(每个 Feature = 一次全屏 Pass)
Shader 特效(溶解、流光、边缘光) 优先用 Shader 做,比堆粒子省得多
VFX Graph 计算在 GPU,适合超大量粒子,但需 GPU 支持 Compute,移动端要谨慎评估

1.8 特效优化 Checklist

  • 所有粒子系统关闭 CollisionLightsShadows
  • 关掉所有未使用的模块
  • Max Particles 有明确上限并归档
  • 粒子贴图 ≤ 256,关 Mipmap、关 Read/Write、ASTC 压缩
  • 常用粒子贴图合并图集,共享材质
  • 用 Mobile/Unlit 粒子 Shader,不用 Standard/PBR
  • 特效走对象池 + 启动预热 + ShaderVariant WarmUp
  • 有同屏数量上限 + 画质分级
  • Overdraw 视图检查过(无大面积亮白区域)
  • 全屏特效数量受控,不叠加

第二章 模型优化

2.1 面数预算与减面

对象 移动端参考面数(三角面)
主角 3,000 ~ 8,000
精英怪 / NPC 2,000 ~ 5,000
小怪(会成群) 800 ~ 2,000
武器 / 道具 200 ~ 800
建筑(SLG) 500 ~ 3,000
同屏总面数 10 万 ~ 30 万

减面手段

  • 删除看不见的面(模型内部、底面、被遮挡部分)。
  • Normal Map 表现细节,代替真实高模。
  • 远景模型用低模 + Imposter / Billboard。
  • 导出前做 Mesh 简化(Blender Decimate / Simplygon)。

2.2 LOD(Level of Detail)

  • LOD Group 组件,3~4 级(LOD0 高模 → LOD1 中 → LOD2 低 → Culled)。
  • 切换阈值按屏幕占比设置(不是距离),通常 60% / 30% / 10%。
  • LOD 生成工具:Unity 内置 LOD Generator、第三方(Mesh Baker、Simplygon)。
  • 注意:LOD 会增加包体和内存,是"用空间换时间"。
  • 大量远景重复物件:LOD 最后一级用 Billboard / Imposter(一张贴图 + 两个三角面)。

2.3 材质与合批(DrawCall 治理核心)

一个模型 = 一个材质是黄金准则。多材质 = 多 DrawCall(SkinnedMesh 尤其明显,每个材质一次蒙皮绘制)。

合批方式 适用场景 代价
静态合批 Static Batching 静态场景(建筑、地形装饰) 内存 + 包体增大(网格被合并复制一份)
动态合批 Dynamic Batching 大量小网格(< 300 顶点 / 900 顶点属性) CPU 每帧变换顶点,有开销;限制多
GPU Instancing 大量相同网格 + 相同材质(草、石头、士兵) 需要 Shader 支持 INSTANCING
SRP Batcher(URP/HDRP 默认开) 通用,按 Shader 变体合批 要求 Shader 的 CBUFFER 结构一致

降级顺序建议:优先 SRP Batcher + GPU Instancing,其次静态合批,动态合批只在小物件上用。

其他

  • 合并 Mesh:静态装饰物在 DCC 或运行时用 Mesh.CombineMeshes 合并成一个(配合图集)。
  • 材质数量控制:一个场景材质种类越少越好。

2.4 骨骼与蒙皮(SLG / ARPG 重点)

SkinnedMeshRenderer 是 CPU + GPU 双重开销大户:

优化点 做法
骨骼数量 移动端控制在 30~60 根以内(人形通常 40~55)
Skin Weights Quality Settings 里设 2 Bones(默认 4),顶点混合计算减半
Update When Offscreen 关闭(或手动扩大 Bounds 保证不被错误剔除)
Skinned Motion Vectors 关闭(除非需要运动模糊/TAA)
Optimize Game Objects 导入时勾选,把骨骼节点从 Transform 层级里剥离,减少层级遍历开销
Mesh Compression 导入设置里开(有精度损失,权衡)
关掉不需要的顶点属性 不需要切线/第二 UV 就设为 None,减少带宽

大量同屏角色(SLG 军团战)方案

  1. GPU Skinning / Bake Mesh:把蒙皮烘焙到贴图,实例化渲染(如 GPU Instancing + Animation Texture)。
  2. Animation Instancing:官方/社区方案,动画数据烘焙成纹理,支持 Instancing。
  3. DOTS Animation + Entities Graphics:ECS 驱动,适合上万单位。
  4. 降级:远处士兵用 Billboard / 简化模型 / 只显示旗帜。

2.5 贴图优化(内存大头)

项目 建议
尺寸 主角 512~1024;小物件 / 场景道具 256;地面 512~1024 tiling;UI 见第三章
必须是 2 的幂(POT) 才能用最好的压缩格式
压缩格式 Android:ASTC(6×6 或 8×8);iOS:ASTC(PVRTC 作兼容降级);PC:DXT/BC7
绝不用 RGBA32 / RGB24 未压缩 一张 1024 RGBA32 = 4MB,ASTC 6×6 约 0.37MB,差 10 倍
关闭 Mipmap UI、2D、正交相机物件、始终同距离的贴图(省 33% 内存 + 带宽)
关闭 Read/Write Enabled 开启后 CPU 内存里会多留一份副本 → 内存 ×2
通道复用 Mask 放 Alpha;金属度/粗糙度/AO 合并到一张 ORM 贴图(3 张变 1 张)
图集 模型贴图也可以打 Atlas(尤其同种建筑/小怪)
Aniso Level 只在地面/斜视角贴图开(1~4),其余设 0
Filter Mode 像素风用 Point,其余 BilinearTrilinear 更贵
Max Size 覆盖 按平台覆盖(iOS/Android 分别设),避免美术给 2048 直接用

2.6 导入设置规范(用 AssetPostprocessor 强制)

csharp 复制代码
// 工程化做法:资源导入时自动修正,不靠人自觉
public class ModelImportRule : AssetPostprocessor
{
    private void OnPreprocessModel()
    {
        var importer = assetImporter as ModelImporter;
        importer.meshCompression = ModelImporterMeshCompression.Medium;
        importer.optimizeMeshVertices = true;
        importer.isReadable = false;              // 关 Read/Write,省一半内存
        importer.importLights = false;
        importer.importCameras = false;
        importer.animationCompression = ModelImporterAnimationCompression.Optimal;
        if (!assetPath.Contains("Characters"))    // 静态模型不导动画
            importer.importAnimation = false;
    }

    private void OnPreprocessTexture()
    {
        var importer = assetImporter as TextureImporter;
        importer.isReadable = false;
        importer.mipmapEnabled = assetPath.Contains("UI") ? false : true;
        importer.maxTextureSize = 1024;
        // 按平台覆盖 ASTC
        var android = importer.GetPlatformTextureSettings("Android");
        android.overridden = true; android.format = TextureImporterFormat.ASTC_6x6;
        importer.SetPlatformTextureSettings(android);
    }
}

2.7 场景与剔除

  • 视锥剔除:自动,但如果模型 Bounds 过大(带动画的角色)会失效 → 手动调 Bounds。
  • 遮挡剔除 Occlusion Culling:室内、城市场景收益极大;开放大地图收益低(烘焙成本高、内存占用大)。
  • 手动分层剔除 / 分块加载:SLG 大地图按 Chunk 加载卸载(见 SLG 算法文档)。
  • 摄像机远裁剪面:调小 Far Clip Plane(雾效配合遮掩)。
  • Layer 剔除Camera.cullingMask 关闭不需要渲染的层。

2.8 模型优化 Checklist

  • 面数符合预算,删除不可见面
  • 主角/重要 NPC 有 LOD,远景有 Imposter
  • 单模型材质数 ≤ 2(理想 1)
  • 骨骼数 ≤ 60,Skin Weights 设 2
  • 蒙皮网格关闭 Update When Offscreen / Motion Vectors
  • 贴图 2 的幂 + ASTC 压缩 + 关 Read/Write + UI 关 Mipmap
  • 用 AssetPostprocessor 强制导入规范
  • 静态物件走静态合批 / SRP Batcher,重复物件走 GPU Instancing
  • Occlusion Culling(适用场景)已烘焙
  • 大量同屏角色有 GPU Skinning / Instancing / DOTS 方案

第三章 UI 优化(UGUI)

3.1 Canvas 拆分:按更新频率分组(不是按 prefab)

Canvas 是 UGUI 合批与重建的基本单位

  • Canvas 内任一元素变化 → 整个 Canvas 标记 dirty → Canvas.BuildBatch 重建所有元素几何。
  • 不同 Canvas 之间不能合批 → Canvas 越多 DrawCall 越多。

正确分组

复制代码
Canvas_Static    背景、框架、固定按钮        ← 几乎不重建
Canvas_Dynamic   血条、倒计时、飘字、CD      ← 高频变化,隔离重建
Canvas_Panel     需要整体开关的整屏面板      ← 只在整屏面板才独立

禁止:每个 UI prefab 都自带一个 Canvas(DrawCall 随 prefab 数线性暴涨)。详见《面试技术文档》第十七章。

3.2 合批规则(打断合批的因素)

同一 Canvas 下能合批的条件:同材质 + 同图集(纹理)+ 同裁剪状态 + 层级连续不被打断

打断合批的常见原因:

打断源 说明
不同图集 / 不同纹理 最常见。Text 用字体图集,Image 用 UI 图集 → 交错就打断
Mask(Stencil) 打断 + 额外 2 个 DrawCall
不同材质(自定义 UI Shader) 每个材质一个批次
层级穿插 A(图集1) - B(图集2) - A(图集1) → 无法合成一批,必须调层级
不同 Rect Clipping 状态 Mask 内外不能合批

验证Window > Analysis > Frame Debugger,逐个看 DrawCall 与合批情况。

3.3 Mask vs RectMask2D

Mask RectMask2D
原理 Stencil 缓冲 + 模板测试 矩形裁剪(Shader 里做 clip)
额外 DrawCall +2(写 stencil + 恢复) 0
合批 打断合批 不额外打断
形状 任意(配合 Image 做圆形/异形头像) 仅矩形
适用 头像、异形裁剪 ScrollView、列表、面板裁剪

结论 :能用 RectMask2D 就用;只有异形裁剪才用 Mask,且避免嵌套 Mask。

3.4 图集(Sprite Atlas)

  • 同屏功能模块分图集(主界面一套、战斗 HUD 一套、背包一套),同屏内尽量同图集 → 合批率最高。
  • 避免同一张图被打进多个图集(内存翻倍)。
  • 图集尺寸 ≤ 2048(超过会有兼容问题 + 内存大)。
  • Padding 设 2~4(防止压缩采样溢出)。
  • 不用的图集勾选 Include in Build 关闭 / 走 Addressables 按需加载。
  • 图集 Variant(SD/HD)按机型切换。

3.5 Text 优化

  • 用 TextMeshPro(TMP) :SDF 渲染,缩放不糊,合批好,功能强。旧 Text 已不推荐。
  • 避免频繁改动文本内容 :改 Text 会触发重新布局 + 几何重建。
    • 数字变化(伤害、金币)→ 拆成独立的 Text 组件,只重建那一个。
    • 值没变就不要赋值if (v != old) text.text = v)。
  • 关闭 Best Fit(每次要计算多次字号,很贵)。
  • 关闭 Rich Text(不需要富文本时)。
  • 字体图集缺字会触发重建 → 预热常用字符集,或用静态字体资源。
  • 避免用 Text 做纯装饰(用 Image + 图集)。
  • Raycast Target 关闭(Text 不需要点击时)。

3.6 ScrollRect / 长列表

  • 虚拟列表(对象池):只实例化可见区域 + 上下各缓冲 2~3 个,滚动时复用。这是长列表最关键的优化。
  • 避免 Content Size Fitter + Layout Group 嵌套 :布局重建成本极高(LayoutRebuild),尤其是 Horizontal/Vertical Layout Group + Content Size Fitter 组合。
    • 替代:手动计算尺寸,或只在初始化时算一次。
  • 滚动时暂停列表项的复杂更新(如加载头像)。
  • RectMask2D 而非 Mask
  • 列表项 prefab 不带自己的 Canvas。

3.7 事件与射线检测

  • 关闭所有不需要的 Raycast TargetImageText 默认开启,每一帧的点击检测都会遍历它们。这是 UI 最简单也最容易忘的优化。
  • CanvasGroup.blocksRaycasts 一次性控制整组。
  • 减少 GraphicRaycaster 数量(每个 Canvas 一个)。
  • 用 Layer 剔除:EventSystem 只处理 UI 层。
  • 3D 物体不需要点击就关掉 Collider 或设到不参与射线的层。

3.8 UI 动效

  • anchoredPosition(位置)比改 sizeDelta/布局便宜(后者触发布局重建)。
  • 频繁变化的 UI 单独放一个 Canvas(动静分离)。
  • 用 DoTween / 缓动库,避免在 Update 里手动插值。
  • 粒子特效在 UI 上:见第十七章(RenderTexture 方案 / 多 Canvas 排序)。

3.9 全屏 UI 时的降级

  • 全屏 UI 打开时关闭或降频 3D 相机Camera.enabled = false 或降低渲染频率)。
  • 背包/商城等纯 UI 界面,背景场景可以不渲染(或用截图/模糊贴图代替)。
  • 半透明遮罩叠加层数控制(Overdraw)。

3.10 UI 优化 Checklist

  • Canvas 按更新频率分组,数量受控(几十个以内)
  • 同屏 UI 尽量同图集,层级排好不穿插
  • 用 RectMask2D 替 Mask
  • 所有非交互 Image/Text 关闭 Raycast Target
  • 用 TMP,关闭 Best Fit / Rich Text
  • 文本变化做"值变了才赋值"
  • 长列表用虚拟列表 + 对象池
  • 避免 LayoutGroup + ContentSizeFitter 嵌套
  • Frame Debugger 验证过 DrawCall

第四章 编程与内存优化

4.1 Unity GC 机制要点

  • Unity 传统使用 Boehm GC非分代、非压缩 → 分配越多碎片越严重,堆只增不减。
  • Unity 2019+ 支持 Incremental GC(增量 GC):把一次大的 GC 分摊到多帧,消除尖刺(但总量不变)。
  • GC 触发时机:托管堆分配达到阈值时自动触发,或手动 GC.Collect()
  • 目标不是"少触发 GC",而是"每帧零分配"(Profiler 的 GC Alloc 列恒为 0)。

4.2 每帧 GC 分配的常见来源(面试必背)

来源 说明 解法
闭包 / lambda 捕获 捕获外部变量 → 生成显示类(class,堆) 用静态本地函数 / 显式传参 / 缓存委托
装箱 struct → object / 接口 / 非泛型集合 泛型集合、实现 IEquatable<T>、避免 ArrayList/object[]
字符串拼接 +$""string.Format 都产生临时字符串 StringBuilder(缓存复用)、避免每帧拼;值不变就别重拼
LINQ 迭代器对象 + 委托分配 热路径改用 for 循环
new class / 数组 每次 new 都分配 对象池、缓存数组、ArrayPool<T>
协程 StartCoroutine 有分配;yield return new WaitForSeconds() 每次 new 缓存 WaitForSeconds 实例;或改用统一 Tick 管理器
Unity API 返回数组 GetComponents<T>()mesh.verticesFindObjectsOfType 都返回新数组 GetComponentsInChildren(list) 复用 List;缓存结果
foreach 老集合 部分老版本对 Dictionary 等有分配(新版已优化) 关键路径用 for / struct 枚举器
enum 作 Dictionary key 默认 comparer 装箱 自定义 IEqualityComparer<T>
委托/事件频繁增删 +=/-= 会产生新委托对象 OnEnable/OnDisable 成对处理,不在 Update 里做
renderer.material 每次访问都会 clone 一份新材质 sharedMaterial(共享),或只取一次缓存
物理查询返回数组 Physics.RaycastAll / OverlapSphere RaycastNonAlloc / OverlapSphereNonAlloc + 预分配数组

4.3 具体编码优化手法

A. 缓存一切

csharp 复制代码
// ❌ 每帧都做
void Update() {
    GetComponent<Rigidbody>().velocity = v;
    Camera.main.transform.position = p;         // Camera.main 内部是 FindObjectWithTag!
    transform.position = p;                     // transform 属性有开销,缓存一下更稳
}

// ✅ 缓存
private Rigidbody _rb; private Transform _tf; private Camera _cam;
void Awake() { _rb = GetComponent<Rigidbody>(); _tf = transform; _cam = Camera.main; }
void Update() { _rb.velocity = v; _tf.position = p; _cam.transform.position = p; }

B. 避免 Update:事件驱动

csharp 复制代码
// ❌ 每帧轮询
void Update() { if (_hp <= 0) Die(); }

// ✅ 事件驱动(SO 事件通道 / C# event)
_health.OnValueChanged += OnHealthChanged;
void OnHealthChanged(float v) { if (v <= 0) Die(); }

C. 合并 Update(大量对象时)

csharp 复制代码
// 1000 个单位各自 Update → 1000 次跨语言调用开销 + cache miss
// ✅ 统一由管理器 Tick
public class UnitManager : MonoBehaviour {
    private readonly List<Unit> _units = new();
    private void Update() { for (int i = 0; i < _units.Count; i++) _units[i].Tick(Time.deltaTime); }
}

D. 对象池

csharp 复制代码
public class ObjectPool<T> where T : Component
{
    private readonly Stack<T> _pool = new();
    private readonly T _prefab;
    private readonly Transform _root;

    public T Get() {
        T item = _pool.Count > 0 ? _pool.Pop() : Object.Instantiate(_prefab, _root);
        item.gameObject.SetActive(true);
        return item;
    }
    public void Return(T item) {
        item.gameObject.SetActive(false);
        _pool.Push(item);
    }
}

用于:子弹、特效、飘字、列表项、怪物、网络包。

E. 字符串与哈希

  • Animator.SetTrigger("Attack") → 先用 Animator.StringToHash("Attack") 缓存 int。
  • 比较用 gameObject.CompareTag("Enemy") 而不是 tag == "Enemy"(后者分配)。
  • Shader 属性用 Shader.PropertyToID 缓存。

F. 物理优化

  • 简化 Collider:Box / Sphere / Capsule > MeshCollider(MeshCollider 极贵,静态场景除外)。
  • Layer Collision Matrix 关闭不需要的层间碰撞。
  • 调大 Fixed Timestep(0.02 → 0.025/0.033),减少物理步数。
  • 关闭 Auto Sync Transforms(Unity 2018+ 默认关,开了会每次查询都同步所有变换)。
  • NonAlloc 系列 API。
  • 不需要物理就别加 Rigidbody

G. 计算密集型任务

  • Job System + Burst:寻路(A*)、视野计算、大量单位位置更新、骨骼计算。零 GC、SIMD 加速。
  • NativeArray / NativeListAllocator.Persistent 长驻,避免频繁分配)。
  • 分帧:大循环拆到多帧执行(如一次生成 500 个单位,分 10 帧)。

H. 避免反射与消息

  • SendMessage / BroadcastMessage / Invoke(string) 走反射,又慢又有分配 → 用委托 / 事件 / 接口 / SO 事件通道。

4.4 内存占用与泄漏

内存构成

类别 大头
Native(引擎) 纹理、Mesh、AudioClip、AnimationClip、Shader 变体、AssetBundle
Managed(托管堆) C# 对象,只增不减
Lua/ILRuntime/HybridCLR 热更层内存(SLG 常见)
其他 RenderTexture、GraphicsBuffer、字体

常见泄漏源

  1. 事件未退订publisher.OnX += Handler 后没 -= → 发布者一直持有订阅者 → 对象无法回收。
  2. 静态引用:静态字段 / 单例缓存持有对象 → 永不释放。
  3. 协程未停止 :对象销毁但协程还在跑(协程随 MonoBehaviour 停止,但 StopAllCoroutines 要主动调)。
  4. renderer.material 克隆:每次访问生成新材质,不 Destroy 就泄漏。
  5. new RenderTexture 不释放。
  6. AssetBundle / Addressables 只加载不释放(句柄没 Release)。
  7. 未使用的资源常驻Resources 目录全部打包且不卸载。
  8. 重复资源:同一张图被多个 AB / 图集各打一份。

排查工具

  • Memory Profiler(Package):看托管堆快照、引用链(谁持有谁)、找泄漏根。
  • Profiler Memory 面板:Simple / Detailed 模式看各资源占用。
  • 关键做法 :对比两次快照的 Diff(进战斗前 / 出战斗后),多出来的就是泄漏。

卸载策略

  • Resources.UnloadAsset(asset):卸载单个资源(纹理等)。
  • Resources.UnloadUnusedAssets()会卡顿,只在切场景 / Loading 界面调用。
  • Addressables:Addressables.Release(handle),引用计数管理。
  • AssetBundle:bundle.Unload(true)(连资源一起卸)/ Unload(false)(只卸 bundle 头,资源手动管)。

音频内存(常忽略):

Load Type 特点
Decompressed on Load 解压到内存,播放快,内存大(适合短音效)
Compressed in Memory 压缩驻留,播放时解压(CPU 换内存)
Streaming 流式读取,内存最小(适合 BGM)
  • 勾选 Force To Mono(音效一般不需要立体声,省一半)。
  • Sample Rate(44100 → 22050)。
  • BGM 用 Streaming + 压缩。

动画内存

  • AnimationClip 压缩(Optimal),误差阈值调大(0.5 → 1.5 肉眼基本无感)。
  • Animator Controller 状态过多会增加内存和求值成本。
  • 不需要的 clip 不打包。

4.5 编程与内存 Checklist

  • Profiler 的 GC Alloc 列每帧为 0
  • 无 LINQ / 无每帧字符串拼接 / 无闭包捕获
  • 所有 GetComponent、Camera.main、Transform 已缓存
  • 协程的 WaitForSeconds 已缓存,或用统一 Tick
  • 子弹/特效/飘字/列表项全部对象池化
  • 物理用 NonAlloc API + 简化 Collider + 层碰撞矩阵已裁剪
  • 事件在 OnEnable/OnDisable 成对订阅退订
  • renderer.material 滥用,改用 sharedMaterial 或缓存
  • Memory Profiler 做过进/出战斗快照 Diff,无泄漏
  • 音频 Force To Mono + 合理 Load Type
  • AnimationClip 已压缩

第五章 渲染与图形优化

5.1 渲染管线设置(URP Asset)

设置 建议
Render Scale 0.8 ~ 1.0(低端机动态降到 0.6)
MSAA 移动端关闭(用 FXAA / SMAA 或直接不用),MSAA 带宽高
HDR 移动端关闭
Depth Texture / Opaque Texture 不需要(无软粒子、无屏幕扭曲)就关
Shadow Distance 20 ~ 50,越短越好
Shadow Cascades 2 级(不要 4 级)
Shadow Resolution 1024 / 2048,按需
Soft Shadows 尽量用 Hard(Soft 多一次采样)
Render Features 不用的全部删除
LOD Bias / Maximum LOD Level 低端机降低 LOD Bias,提前切低模

5.2 光照

  • 烘焙优先:静态场景用 Baked GI / Lightmap,运行时零成本。
  • 动态物体Light Probe 取间接光。
  • 移动端实时光:1 个方向光(主光),点光/聚光灯尽量不用或极少。
  • 关闭不需要的 Realtime Global Illumination
  • 假阴影:角色脚下用一个圆形贴片(Blob Shadow)代替实时阴影,成本几乎为零 ------ 移动端常规做法。

5.3 Shader 优化

要点 说明
变体控制 Shader 变体数爆炸 → 包体大、内存大、加载慢。剔除无用变体(URP Stripping 设置)
变体预热 ShaderVariantCollection.WarmUp() 在启动时调用,避免运行时编译卡顿
精度 移动端用 half / fixed,少用 float(省带宽和 ALU)
纹理采样次数 合并贴图通道,减少 sampler
避免 discard / clip 破坏 Early-Z,移动 GPU(TBDR)代价大。Alpha Test 有时比 Alpha Blend 还慢
避免 GrabPass 会强制同步等待,移动端极贵
简化光照模型 移动端用 Lambert / 简化 PBR,不用完整 PBR + IBL
避免复杂分支 GPU 分支代价高,用 lerp/step 代替 if
Shader 编译 打包时预编译(Preloaded Shaders

5.4 DrawCall 与填充率

DrawCall 高 :合批(SRP Batcher / GPU Instancing / 静态合批)、合并材质、减少物体数。

SetPass 高 :材质种类太多、Shader 变体多 → 统一材质、减少变体。

填充率高(GPU 瓶颈):减少 Overdraw(透明层、粒子、后处理)、降低分辨率(Render Scale / Dynamic Resolution)、减少全屏 Pass。

排序规则

  • 不透明:从前到后(Early-Z 剔除被遮挡像素)。
  • 透明:从后到前(保证混合正确),且不能写 ZWrite

5.5 耗电与发热控制

  • 锁帧Application.targetFrameRate = 30(非竞技类),比跑满 60 省一半电。
  • 动态降帧:检测到帧率持续低于阈值或温度升高 → 降帧 + 降分辨率 + 降画质档。
  • 动态分辨率 (URP ScalableBufferManager / Dynamic Resolution)。
  • 减少持续高负载:粒子、后处理、实时阴影在低端机降级。
  • 菜单/静置时降帧:无操作时降到 15~30 帧。

5.6 渲染优化 Checklist

  • URP Asset:关 MSAA/HDR/无用 Depth Texture、Shadow Distance 与 Cascade 收敛
  • 无用的 Render Feature 已删除
  • 静态光照已烘焙,实时光仅 1 盏主光
  • Shader 变体已裁剪 + 启动预热
  • 移动端 Shader 用 half 精度、无 discard、无 GrabPass
  • DrawCall / SetPass 在预算内
  • 有动态分辨率 / 动态降帧策略
  • 低端机有独立画质档

第六章 包体优化

方向 做法
纹理 ASTC 压缩、Max Size 平台覆盖、关 Mipmap、关 Read/Write、合并图集
音频 压缩格式(Vorbis/ADPCM)、Force Mono、降采样、BGM 走 Streaming 或下载
模型 Mesh Compression、关 Read/Write、删无用顶点属性、动画压缩
代码 IL2CPP + Managed Stripping Level(High),用 link.xml 保留反射用到的类型
资源分包 Addressables:首包只放必要内容,其余热更下载
剔除重复 用 Build Report / AssetBundle 分析工具找重复资源(同一张图进多个 AB)
字体 裁剪字库(只保留常用字 + 游戏用字),避免完整中文字体(几 MB ~ 十几 MB)
视频 走 StreamingAssets 或远端,不进首包
场景 精简场景数量,复用 prefab
Lua/热更 HybridCLR + AB 热更,减少首包体积

第七章 开发技巧总结

7.1 架构技巧(解耦 / 可扩展)

技巧 说明
ScriptableObject 驱动配置 数值、事件、配置全在 SO,策划/美术在 Inspector 改,不改代码、不重编译
SO 事件通道 跨系统通信不依赖引用(GameEvent + GameEventListener),消灭 FindObjectOfType 和单例
RuntimeSet 追踪场上实体(所有敌人、所有友军),替代 FindObjectsOfType 和单例列表
单职责组件 一个 MonoBehaviour 只干一件事;超过 150 行就该拆
自包含 prefab 拖到空场景就能跑,不依赖外部层级
状态机 角色状态、UI 面板、游戏流程(Procedure)都用状态机,逻辑清晰可扩展
对象池统一管理 一个 PoolManager 管所有池,统一预加载、统一上限、统一回收
命令模式 撤销/重做、战斗回放
配置表 + 代码生成 Excel → 自动生成 C# 数据类 + 索引字典,避免手写差错
模块化热更 HybridCLR 分程序集(GameLogic / UI / Config),按域热更

7.2 性能技巧(口诀版)

池化一切、预加载、分帧做、能合批就合批、能剔除就剔除、能烘焙就烘焙、能降级就降级。

  • 池化:子弹、特效、飘字、列表项、怪物、网络包。
  • 预加载:特效预热、Shader 变体预热、常用资源进场景前加载。
  • 分帧:大循环、大资源加载、批量实例化分到多帧。
  • 合批:SRP Batcher + GPU Instancing + 静态合批 + 图集。
  • 剔除:视锥、遮挡、距离、Layer、手动分块。
  • 烘焙:光照、阴影、导航网格。
  • 降级:LOD、画质档、动态分辨率、动态降帧。

7.3 避坑清单(血泪经验)

  1. Editor 流畅 ≠ 真机流畅。所有性能结论必须真机验证。
  2. renderer.material 会克隆 ------ 用 sharedMaterial(但注意:改 sharedMaterial 会影响所有共用者)。
  3. 静态字段不随场景切换清空 ------ 跨场景残留是常见 bug 源。
  4. 事件必须成对订阅/退订 ------ OnEnable/OnDisable,不在 Awake/Start 里订阅后不管。
  5. 不要在 Updatenew 任何东西
  6. Camera.mainFindObjectWithTag ------ 每帧调用就是在查找。
  7. 不要在 Update 里用 GetComponent
  8. 协程不随 SetActive(false) 停止(随 Destroy 停止),隐藏对象时协程仍在跑。
  9. 改变父节点会触发完整重建(Transform 层级变化)。
  10. Destroy 是延迟的 ------ 不是立即销毁,本帧内对象还在。

7.4 工具链

工具 用途
Profiler(CPU / GPU / Memory / Rendering) 全能主力,真机连接
Profile Analyzer 对比两帧/两段时间的数据,量化优化收益
Frame Debugger 逐 DrawCall 看渲染流程、找合批失败原因
Memory Profiler 内存快照、引用链、泄漏定位、Diff 对比
Overdraw 视图 / RenderDoc / Xcode GPU Capture 填充率分析
自建 Debug 面板 实时显示 FPS、DrawCall、GC Alloc/帧、对象池占用、内存 ------ 开发期必备
CI 性能基线 每次构建自动跑固定场景录性能数据,超标报警
AssetPostprocessor 强制资源规范(尺寸/压缩/ReadWrite),从源头防劣化

7.5 面试速答模板:完整版"你怎么做性能优化?"

"我做优化分四步:测量、定位、修改、验证。第一,先连真机 Profiler 取基线,我会分四象限判断瓶颈------CPU 逻辑、渲染提交、GPU 填充率还是 GC。常用的快速判定是把 Render Scale 降到 50%,帧率暴涨说明瓶颈在填充率,几乎不变说明在 CPU 或顶点。

第二,针对瓶颈逐项治理。GC 方面 ,我的目标是每帧零分配------避免闭包捕获、装箱、字符串拼接、LINQ,缓存 GetComponent 和 Camera.main,协程的 WaitForSeconds 缓存复用,物理用 NonAlloc 接口,子弹特效飘字全部对象池化。渲染方面 ,控制 DrawCall 和 SetPass:静态物件走静态合批或 SRP Batcher,重复物件走 GPU Instancing,材质和图集合并,UI 按更新频率拆 Canvas 做动静分离,Mask 换 RectMask2D。内存方面 ,纹理用 ASTC 压缩、关 Read/Write 和不必要的 Mipmap,动画压缩,音频 Force Mono,用 Memory Profiler 做进/出战斗的快照 Diff 查泄漏,特别注意事件没退订、静态引用、renderer.material 克隆这几个泄漏源。特效方面重点是 Overdraw,关掉 Collision 和阴影、限制 Max Particles、贴图图集化、走对象池和启动预热,配合画质分级和同屏数量上限。

第三,改完必须复测对比数据,我会用 Profile Analyzer 量化前后收益。

最后一步是防回归:资源规范用 AssetPostprocessor 在导入时强制,性能数据进 CI 做基线对比。

我还想强调一点:优化都有代价------合批吃内存、LOD 吃包体、降分辨率吃画质,所以每次我都会说清楚这次是拿什么换了什么。"


附录:一页纸总清单(打印用)

特效

关 Collision/Shadow/Light · 限制 Max Particles · 关无用模块 · 贴图 ≤256 + ASTC + 关 Mipmap/ReadWrite · Mobile/Unlit Shader · 粒子图集 · 对象池 + 预热 + WarmUp · 同屏上限 + 画质分级 · Overdraw 视图验收

模型

面数按预算 · LOD + Imposter · 单模型 ≤2 材质 · 骨骼 ≤60 + SkinWeights 2 · 关 UpdateWhenOffscreen/MotionVectors · POT + ASTC + 关 ReadWrite · 通道复用 ORM · AssetPostprocessor 强制规范 · 静态合批/Instancing/SRP Batcher · Occlusion Culling · 大量角色 GPU Skinning / DOTS

UI

Canvas 按频率分组(非按 prefab) · 同图集不穿插 · RectMask2D 替 Mask · 关 Raycast Target · TMP + 关 BestFit/RichText · 值变了才赋值 · 虚拟列表对象池 · 避免 LayoutGroup+ContentSizeFitter 嵌套 · Frame Debugger 验收

编程内存

GC Alloc/帧 = 0 · 缓存 GetComponent/Camera.main/Transform · 无 LINQ/闭包/拼接/装箱 · 对象池 · NonAlloc 物理 · 简化 Collider + 层碰撞矩阵 · 事件成对退订 · sharedMaterial 不克隆 · Memory Profiler Diff 查泄漏 · 音频 ForceMono · 动画压缩

渲染

URP:关 MSAA/HDR/无用 DepthTexture · Shadow Distance+Cascade 收敛 · 删无用 RenderFeature · 烘焙光照 + LightProbe · 1 盏实时主光 · Blob 假阴影 · Shader 变体裁剪+预热 · half 精度 · 无 discard/GrabPass · 动态分辨率 + 动态降帧

方法

先测量后优化 · 降分辨率判 GPU 瓶颈 · 真机验证 · 改一处复测一次 · Profile Analyzer 量化 · AssetPostprocessor 防劣化 · CI 性能基线


相关推荐
maybeyaluokenai1 小时前
unity build in渲染管线概述
unity·图形渲染
学习星球1 小时前
AI 原生游戏开发:Godot 4 + AI Agent 全栈指南(Ziva 3 / Godot MCP 深度实战)
人工智能·游戏引擎·godot
云雨巫山1 小时前
Unity 性能优化 · 图解全景手册
unity·性能优化·游戏引擎
show4333 小时前
2026小程序端视频处理性能优化:批量+120分钟大文件
性能优化·音视频
Anhty15 小时前
2026九月最新变声器测评:iOS安卓双端适配,低延迟运行更稳定
android·人工智能·功能测试·ios·智能手机
Madokaly16 小时前
DOTween的Vector3Plugin
unity
图扑软件18 小时前
中篇・运笔|统一 DataModel 底座,HT UI 组件万物同源
javascript·低代码·ui·性能优化·数据可视化
知见漫记20 小时前
AI文档总结折叠手机推荐,联想moto razr Fold让信息处理更高效
人工智能·智能手机
raindayinrain21 小时前
理解网络--Tcp性能优化
网络·tcp/ip·性能优化