mcpField
U
unity-performance

unity-performance

github.com/nice-wolf-studio开发4090 次安装7 周前更新
代码仓库

关于 unity-performance

面向 Unity 开发者的性能分析与优化参考技能,介绍 Profiler、内存分析和 Frame Debugger 等工具。内容涵盖 CPU、GPU、内存、渲染及脚本优化,并提供性能分析实践。

主要功能

  1. 01Profiler 与性能分析工具概览
  2. 02CPU、GPU 和内存指标解读
  3. 03GC 分配与资源内存优化
  4. 04脚本、物理与渲染优化
  5. 05Frame Debugger 绘制调用检查
  6. 06设备性能分析实践

适用场景

  1. 01在目标设备上定位帧时间瓶颈
  2. 02追踪游戏过程中的 GC 分配
  3. 03检查批处理中断与过度绘制
  4. 04优化纹理、网格和音频资源占用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-performance

安装后在 Claude Code 中输入/unity-performance确认已加载。

附带文件references/memory-optimization.mdreferences/profiler-guide.md

详细介绍

Unity 性能分析与优化

基于 Unity 6.3 LTS(6000.3)官方文档

核心工具概览

Unity 提供了多种内置工具和基于软件包的工具,用于性能分析与优化:

工具访问方式用途
Unity ProfilerWindow > Analysis > Profiler按帧查看 CPU、GPU、内存和渲染指标
Memory Profiler软件包:com.unity.memoryprofiler深度内存快照、对比和泄漏检测
Frame DebuggerWindow > Analysis > Frame Debugger检查渲染帧中的每一次绘制调用
Profile Analyzer软件包:com.unity.performance.profile-analyzer对多个 Profiler 帧进行统计分析

Unity Profiler

Profiler 会捕获逐帧性能数据。它可以在 Editor 中运行,也可以连接到目标设备上的 Development Build。

CPU 模块

CPU 模块是查找性能瓶颈的主要工具。

视图:

  • Timeline view -- 在时间轴上并排显示所有线程。最适合查看线程争用、作业系统利用率以及帧时间的分配情况。
  • Hierarchy view -- 将采样数据汇总成调用树。显示 Total %、Self %、Calls、GC Alloc 和 Time 列。最适合查找开销最大的函数。

关键线程:

  • 主线程 -- 游戏逻辑、脚本、动画、UI 布局
  • 渲染线程 -- 向 GPU 提交绘制调用(SRP 命令缓冲区执行)
  • Job Worker 线程 -- C# Jobs、Burst 编译的工作负载、物理模拟

Profiler 标记:

  • 内置标记(例如,PlayerLoop、Update.ScriptRunBehaviourUpdate、Physics.Simulate)
  • 使用 Unity.Profiling.ProfilerMarker 自定义标记(参见 references/profiler-guide.md)

深度性能分析:

  • 自动检测每一次 C# 方法调用
  • 警告:会产生 10-100 倍的额外开销,分析结果无法反映实际性能
  • 请改用 ProfilerMarker 进行有针对性的检测

GC 分配的调用堆栈:

  • 在 Profiler 工具栏中启用此功能,即可查看托管分配的来源
  • 会带来适度的额外开销,但对于追踪 GC 峰值非常有用
GPU 模块
  • 按渲染通道显示 GPU 帧时间
  • 识别着色器复杂度和填充率问题
  • 有助于诊断过度绘制和开销较大的片元着色器
  • 注意:并非所有平台都支持 GPU 性能分析(请查阅平台文档)
  • 在移动设备上,请使用平台专用的 GPU 性能分析工具(Xcode GPU Debugger、RenderDoc、Arm Mobile Studio)
内存模块

显示逐帧内存摘要:

  • 托管内存 -- C# 堆(Mono/IL2CPP)、GC 分配
  • 原生内存 -- 纹理、网格、音频剪辑、渲染目标、原生容器
  • 预留总量与已用总量 -- Unity 从 OS 预留的内存量与实际使用的内存量
  • 帧内 GC 分配量 -- 关键指标;游戏运行期间该值非零表示存在分配压力
其他模块
模块关键指标
渲染批次、SetPass 调用次数、三角形、顶点、阴影投射体
物理活动/休眠刚体、接触点、宽相位碰撞对
音频播放中的音源、DSP 负载、内存使用量
UICanvas 重建、布局重建、批次
视频视频播放器解码时间

性能分析最佳实践

  1. 在目标设备上进行性能分析 —— 编辑器开销会显著影响结果。务必在实际硬件上验证。
  2. 使用开发版构建 —— 在 Build Settings 中启用 Development Build 和 Autoconnect Profiler。构建启动后,Profiler 会自动连接。
  3. 使用 ProfilerMarker —— 使用自定义标记对关键代码路径进行插桩,无需承担 Deep Profile 的开销即可精确测量。
  4. 分析有代表性的场景 —— 测试最坏情况的场景(大量敌人、粒子效果、复杂 UI)。
  5. 比较前后数据 —— 使用 Profile Analyzer 对采集结果进行统计比较。
  6. 设定帧预算 —— 明确目标:
    • 60 FPS = 每帧 16.6 ms
    • 120 FPS = 每帧 8.3 ms
    • 30 FPS = 每帧 33.3 ms
  7. 分析 CPU 时禁用 VSync —— VSync 会掩盖 CPU 余量。

内存优化

应避免的 GC 分配模式

每次托管内存分配都会增加 GC 压力。在高频执行路径(Update、FixedUpdate、LateUpdate)中,目标是零分配。

模式问题修复方法
字符串拼接(+)每次都会创建新字符串使用 StringBuilder 或 string.Create
LINQ 查询产生迭代器分配,并可能导致闭包装箱使用手动 for/foreach 循环
捕获局部变量的 Lambda 闭包编译器会生成类分配缓存委托或使用静态 Lambda
装箱(int 转 object)在托管堆上分配内存使用泛型 API,避免使用 object 参数
对非泛型 IEnumerable 使用 foreach枚举器装箱使用强类型集合或 for 循环
params 数组每次调用都会分配数组提供重载或使用 Span<T>
对值类型调用 ToString()会分配字符串缓存结果或避免在高频路径中调用
资源内存

纹理(通常是最大的内存消耗项):

  • 在移动平台上使用 ASTC 压缩,在桌面平台上使用 BC7/DXT
  • 启用 mipmap 流式加载(Texture.streamingMipmaps)以限制 VRAM 使用量
  • 在画质允许的情况下,降低最大纹理尺寸
  • 使用纹理图集以减少绘制调用和内存碎片

网格:

  • 在导入设置中启用网格压缩
  • 使用 LOD Groups 在远处降低顶点数量
  • 在导入设置中移除未使用的顶点通道(切线、颜色)

音频:

  • Decompress on Load —— 播放速度快、内存占用高(仅适用于较短的音效)
  • Compressed in Memory —— 内存占用较低、CPU 开销适中(适用于中等长度的片段)
  • Streaming —— 内存占用极低、持续进行 I/O(适用于音乐、环境音)
  • 根据长度和使用频率为每个片段设置 Load Type

卸载:

  • Resources.UnloadUnusedAssets() —— 释放没有引用的资源(速度较慢,适合在加载界面使用)
  • Addressables:使用 Addressables.Release() 减少引用计数,并在计数归零时卸载
对象池

重复使用频繁创建和销毁的对象,以避免 GC 压力和实例化开销。

using UnityEngine.Pool;

public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;
    private ObjectPool<Bullet> _pool;

    private void Awake()
    {
        _pool = new ObjectPool<Bullet>(
            createFunc: () => Instantiate(bulletPrefab),
            actionOnGet: b => b.gameObject.SetActive(true),
            actionOnRelease: b => b.gameObject.SetActive(false),
            actionOnDestroy: b => Destroy(b.gameObject),
            defaultCapacity: 20,
            maxSize: 100
        );
    }

    public Bullet GetBullet() => _pool.Get();
    public void ReturnBullet(Bullet b) => _pool.Release(b);
}

另请参阅:unity-scripting,了解更多对象池模式。


CPU 优化

脚本
  • 缓存 GetComponent 结果 —— 在 Awake/Start 中调用,并将结果存储在字段中
  • 使用 CompareTag("Tag") —— 避免从 gameObject.tag 产生字符串分配
  • 避免在高频路径中使用 Find 方法 —— Find、FindObjectOfType、FindObjectsByType 开销较大;应缓存引用
  • 使用 C# Jobs + Burst —— 适用于数据密集型处理(空间查询、AI、寻路)。参见 unity-ecs-dots。
  • 使用 Awaitable 执行异步操作 —— 非阻塞式场景加载、资源加载和 Web 请求
// Cache component references
private Rigidbody _rb;
private void Awake() => _rb = GetComponent<Rigidbody>();

// Use CompareTag instead of string comparison
if (other.CompareTag("Enemy")) { /* ... */ }
物理
  • 简化碰撞体 —— 优先使用基本碰撞体(Box、Sphere、Capsule),而非 MeshCollider
  • 基于图层的碰撞矩阵 —— 在 Project Settings > Physics 中禁用不必要的碰撞组合
  • FixedUpdate 时间步长 —— 默认值为 0.02s(50Hz)。对于性能要求较低的游戏,可增大到 0.03-0.04s。
  • Rigidbody 休眠 —— 确保对象进入休眠;避免对静止对象持续调用 AddForce。
  • 无分配查询 —— 使用 Physics.RaycastNonAlloc、Physics.OverlapSphereNonAlloc
渲染
  • SRP Batcher —— 在 URP/HDRP 中默认启用。可降低兼容着色器的 SetPass 调用开销。
  • 静态批处理 —— 将不移动的对象标记为 Batching Static,以便在构建时合并网格。
  • GPU Instancing —— 为大量相同对象(树木、草、碎片)启用材质上的此功能。
  • 动态批处理 —— 在运行时批处理小型网格(在 SRP 中收益有限;优先使用 GPU instancing)。
  • 遮挡剔除 —— 烘焙遮挡数据,以跳过对墙后对象的渲染。
  • LOD Groups —— 降低远处对象的几何复杂度。
  • 着色器变体 —— 尽量减少关键字组合;在 Player Settings 中剥离未使用的变体。

帧调试器

帧调试器(Window > Analysis > Frame Debugger)可让你逐个检查一帧中的每次绘制调用。

工作流:

  1. 打开帧调试器并点击 Enable
  2. Game 视图会冻结在当前帧
  3. 在左侧面板中逐步检查绘制调用
  4. 检查:着色器、通道、关键字、渲染目标、网格、材质属性
  5. 找出冗余绘制、批处理断点和过度绘制

常见用途:

  • 验证 SRP Batcher / 批处理的效果
  • 找出批次中断的原因(不同的材质、着色器关键字、渲染状态变化)
  • 检查阴影通道及其开销
  • 调试透明对象排序问题
  • 验证渲染目标切换(每次切换都会产生开销)
  • 检查全屏后处理通道的数量

IL2CPP 和托管代码性能

Unity 6 默认在大多数平台上使用 IL2CPP。需要注意的性能要点:

  • IL2CPP 提前编译 会将 C# 转换为 C++,相比 Mono JIT 可实现更好的优化
  • 代码剥离 会移除未使用的代码,从而减小构建体积并改善启动速度
  • 泛型共享 —— IL2CPP 会为引用类型泛型共享代码,但会为值类型泛型生成单独的代码。过多的值类型泛型实例化会增大二进制文件体积。
  • 虚方法调用 —— IL2CPP 使用 vtable 分派;sealed 类/方法可实现去虚拟化和内联
  • 结构体布局 —— 对传递给原生代码的数据使用 [StructLayout(LayoutKind.Sequential)]。保持结构体较小(小于 16 bytes),以便高效传递。
// sealed enables devirtualization -- IL2CPP can inline the call
public sealed class FastEnemy : EnemyBase
{
    public override void Think()
    {
        // This can be inlined by IL2CPP when called through a typed reference
    }
}

质量设置和自适应性能

运行时质量切换
// Switch quality level at runtime (e.g., from settings menu)
QualitySettings.SetQualityLevel(qualityIndex, applyExpensiveChanges: true);

// Adjust individual settings
QualitySettings.shadows = ShadowQuality.Disable;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 60;
自适应性能(移动端)

Adaptive Performance 包(com.unity.adaptiveperformance)会根据设备热状态和性能指标动态调整质量:

  • 设备过热时自动降低分辨率、LOD 偏差和阴影质量
  • 提供 IAdaptivePerformance 接口,用于自定义缩放逻辑
  • 支持 Samsung(GameSDK)和通用提供程序

常见性能模式

异步场景加载
using UnityEngine.SceneManagement;

public async Awaitable LoadSceneAsync(string sceneName)
{
    AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
    op.allowSceneActivation = false;

    while (op.progress < 0.9f)
    {
        // Update loading bar: op.progress / 0.9f
        await Awaitable.NextFrameAsync();
    }
    op.allowSceneActivation = true;
}
将工作分散到多个帧
// Process a large list over multiple frames
private IEnumerator ProcessChunked<T>(List<T> items, int chunkSize, System.Action<T> process)
{
    for (int i = 0; i < items.Count; i++)
    {
        process(items[i]);
        if ((i + 1) % chunkSize == 0)
            yield return null; // Resume next frame
    }
}
关注帧预算
using System.Diagnostics;

private readonly Stopwatch _sw = new();
private const float BUDGET_MS = 4f; // Portion of frame budget

private void Update()
{
    _sw.Restart();
    while (_workQueue.Count > 0 && _sw.Elapsed.TotalMilliseconds < BUDGET_MS)
    {
        ProcessNextItem(_workQueue.Dequeue());
    }
}
使用 Additive 场景进行关卡流式加载
using UnityEngine.SceneManagement;

// Load adjacent area additively without blocking
public async Awaitable StreamArea(string sceneName)
{
    AsyncOperation op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
    while (!op.isDone)
        await Awaitable.NextFrameAsync();
}

// Unload area when player moves away
public async Awaitable UnloadArea(string sceneName)
{
    AsyncOperation op = SceneManager.UnloadSceneAsync(sceneName);
    while (!op.isDone)
        await Awaitable.NextFrameAsync();
    // Reclaim memory from unloaded assets
    Resources.UnloadUnusedAssets();
}
自定义 ProfilerMarker 插桩
using Unity.Profiling;

public class AISystem : MonoBehaviour
{
    private static readonly ProfilerMarker s_AIUpdate = new("AISystem.Update");
    private static readonly ProfilerMarker s_Pathfind = new("AISystem.Pathfind");

    private void Update()
    {
        using (s_AIUpdate.Auto())
        {
            foreach (var agent in _agents)
            {
                using (s_Pathfind.Auto())
                {
                    agent.RecalculatePath();
                }
            }
        }
    }
}

常见瓶颈与解决方法

症状可能原因解决方法
FPS 低,CPU 主线程占用高Update 中逻辑过多使用 Profiler 分析;优化、改用 Jobs,或分散到多帧处理
GC 峰值(帧卡顿)热路径中发生分配使用对象池、缓存;避免在 Update 中使用 LINQ/字符串拼接
绘制调用 / SetPass 调用过多未进行批处理或材质过多启用 SRP Batcher、使用 GPU 实例化、使用图集纹理
内存持续增长资源泄漏、Addressables 未释放调用 Resources.UnloadUnusedAssets(),检查 Addressable 引用计数
物理卡顿激活的碰撞体或接触点过多简化碰撞体、使用碰撞矩阵图层、增大时间步长
着色器编译卡顿按需编译着色器变体使用着色器预热(ShaderVariantCollection.WarmUp()),减少变体数量
加载时间过长同步加载资源使用 Addressables.LoadAssetAsync 或 Resources.LoadAsync
渲染线程受限绘制调用或 GPU 命令过多减少批次、简化着色器、降低分辨率

反模式

做法有害原因解决方法
在生产环境中使用 Debug.Log字符串格式化 + 托管分配 + I/O使用 [Conditional("UNITY_EDITOR")],或通过脚本定义剔除
在 Update 中使用 Camera.main每次都会在内部调用 FindGameObjectWithTag在 Awake 或 Start 中缓存
每帧调用 Instantiate/Destroy每次调用都会产生 GC 分配 + 原生开销使用 ObjectPool<T>
在 Update 中调用 GetComponent<T>()每帧都会产生查找开销在 Awake/Start 中缓存
使用 OnGUI 构建游戏 UIIMGUI 每帧重绘,GC 开销大使用 UI Toolkit 或 uGUI(Canvas)
使用 Deep Profile 进行性能测试10-100x 的开销会使结果失效使用 ProfilerMarker 进行针对性检测
在 FixedUpdate 中分配内存默认以物理频率(50Hz)运行,会放大 GC 影响预先分配、使用对象池、使用 NativeArray
使用 SendMessage / BroadcastMessage基于反射,且会产生分配使用直接方法调用、事件或 UnityEvent

关键 API 速查

API命名空间用途
ProfilerMarkerUnity.Profiling零开销的自定义 Profiler 检测
ProfilerRecorderUnity.Profiling在运行时读取 Profiler 计数器值
Profiler.BeginSample / EndSampleUnityEngine.Profiling旧版自定义 Profiler 样本(在非开发版构建中会被剔除)
ObjectPool<T>UnityEngine.Pool通用对象池
ListPool<T> / HashSetPool<T>UnityEngine.Pool临时集合对象池
NativeArray<T>Unity.Collections用于 Jobs/Burst 的非托管、无 GC 数组
NativeList<T>Unity.Collections非托管、可调整大小的列表
StopwatchSystem.Diagnostics高精度计时
ShaderVariantCollectionUnityEngine.Rendering通过着色器预热避免编译卡顿
QualitySettingsUnityEngine在运行时切换质量等级

相关技能

  • 有关脚本编写模式和组件生命周期,请参阅 unity-scripting
  • 有关渲染管线和着色器优化,请参阅 unity-graphics
  • 有关移动端/主机平台优化,请参阅 unity-platforms
  • 有关 ECS、Jobs 和 Burst(面向数据的性能优化),请参阅 unity-ecs-dots
  • 有关物理配置和优化,请参阅 unity-physics
  • 有关 UI 性能(Canvas 重建、批处理),请参阅 unity-ui

附加资源

评论

支持 Markdown