Unity性能优化利器:MaterialPropertyBlock原理、实战与避坑指南

发布时间:2026/8/6 16:57:05
Unity性能优化利器:MaterialPropertyBlock原理、实战与避坑指南 1. 项目概述为什么MaterialPropertyBlock是性能优化的“利器”如果你在Unity项目里做过大量同模型、不同外观比如颜色、贴图的物体渲染并且发现Draw Call绘制调用高得离谱或者内存里塞满了成千上万个几乎一样的材质实例那么MaterialPropertyBlock后文简称MPB就是你一直在找的那个“神器”。它不是什么新概念但绝对是Unity渲染优化工具箱里最被低估、也最实用的工具之一。简单来说MPB是一个轻量级的属性块它允许你在运行时动态地修改渲染器如MeshRenderer、SkinnedMeshRenderer上的材质属性比如颜色_Color、浮点值_Metallic、纹理_MainTex等而不需要为每个需要不同属性的物体都去创建一个独立的材质实例。这个“不需要”就是性能优化的核心。想象一下你有1000个士兵模型他们穿着不同颜色的制服。传统做法是创建1000个材质实例每个实例设置不同的_Color。这会导致1000个材质在内存中并且如果它们不能被动态合批就可能产生大量的Draw Call。而使用MPB你可以让这1000个士兵共享同一个材质然后通过MPB为每个士兵单独设置颜色。内存里只有一个材质Draw Call也有机会被大幅合并性能提升立竿见影。这个工具特别适合移动端开发、大型场景如策略游戏、开放世界中的大量植被/建筑、以及任何需要大量重复物体但又有细微差异的场合。网上很多教程只讲了“怎么用”但没讲透“为什么用”以及“怎么用好”。这篇指南就从实战出发结合完整代码带你彻底搞懂MPB避开那些新手和老手都可能踩的坑。2. MaterialPropertyBlock核心原理与优势深度解析2.1 传统材质实例化 vs. MaterialPropertyBlock要理解MPB的优势必须先明白Unity材质系统的工作原理。在Unity中一个材质Material是Shader及其一套属性Properties的封装。当你把材质拖给一个Renderer组件时Unity内部会为这个Renderer创建一个材质实例Material Instance。即使多个Renderer使用同一个“源”材质资产它们在运行时也是独立的实例。修改任何一个实例的属性比如通过renderer.material.color Color.red都会触发这个实例的创建如果还没创建的话。传统方式的弊端内存开销每个材质实例都占用独立的内存。对于属性差异很小的物体仅颜色不同这会造成巨大的内存浪费。Draw Call增加Unity的静态/动态合批Batching机制要求物体使用相同的材质和材质实例。一旦你为每个物体创建了独立的材质实例即使它们源自同一个材质资产合批也会失效导致Draw Call飙升。GC垃圾回收压力频繁地通过renderer.material获取材质这会在后台创建新实例或销毁物体会产生大量材质实例垃圾触发GC引起卡顿。MaterialPropertyBlock的解决方案MPB绕开了创建独立材质实例的步骤。它允许你将需要覆盖的材质属性存储在一个独立的、轻量的“属性块”对象中然后将这个块附加到Renderer上。在渲染时GPU会优先使用MPB中设置的属性值如果MPB里没有则回退使用材质本身的默认值。核心优势对比特性传统材质实例 (renderer.material)MaterialPropertyBlock内存占用高。每个对象一个材质实例。极低。多个对象可共享同一材质MPB本身开销很小。Draw Call可能很高。独立的材质实例会打断合批。优化潜力大。共享材质为合批创造了条件。属性修改灵活性直接修改实例属性简单直观。通过SetXXX方法设置稍显繁琐但功能强大。对材质资产的影响修改实例属性不会影响磁盘上的材质资产。完全不影响材质资产甚至不影响共享的材质实例。适用场景物体材质属性差异大或需要独立材质功能如不同的Render Queue。大量物体共享同一Shader仅需修改少量属性颜色、纹理、浮点数等。注意MPB并不是万能的。它不能修改Shader的渲染状态如ZWrite, Cull Mode也不能更换Shader本身。这些修改仍然需要创建新的材质实例。2.2 MPB如何与渲染管线协同工作理解MPB在渲染流程中的位置能帮你更好地使用它。当Unity渲染一个带有MPB的物体时大致流程如下收集渲染数据Unity收集所有需要渲染的物体信息包括网格、变换矩阵、材质和附加的MPB。合批判断Unity尝试将使用相同材质和相同MPB结构注意是结构不是值的物体进行合批。这意味着即使两个物体的MPB里_Color值不同一个红一个蓝只要它们都通过MPB设置了_Color这个属性它们仍然有机会被合批。这是MPB性能优势的关键。提交渲染命令Draw Call对于每个合批批次Unity向GPU提交一次绘制命令。GPU渲染在GPU端Shader执行。对于每个属性GPU会检查该物体是否绑定了MPB。如果有则使用MPB中提供的值如果没有则使用材质中定义的默认值。这个过程意味着MPB的数据是在每批次渲染时动态传递给GPU的而不是像材质属性那样“固化”在材质资源里。这种动态性带来了灵活性但也引入了一些限制比如MPB的属性在编辑模式下不可见因为它是运行时数据。3. 实战入门MaterialPropertyBlock基础用法与完整代码示例理论讲完了我们直接上代码。这部分会从最简单的例子开始逐步深入到复杂场景。3.1 基础设置修改颜色与浮点属性假设我们有一个使用Standard Shader的材质我们想动态修改一批物体的主颜色和金属度。using UnityEngine; public class BasicMPBExample : MonoBehaviour { public MeshRenderer targetRenderer; // 需要修改的渲染器 public Color targetColor Color.red; public float targetMetallic 0.5f; private MaterialPropertyBlock _mpb; void Start() { // 1. 创建MaterialPropertyBlock实例 // 最佳实践在Start或Awake中创建并复用避免每帧new。 _mpb new MaterialPropertyBlock(); // 2. 检查渲染器是否已有MPB并获取当前值可选用于混合或初始化 // targetRenderer.GetPropertyBlock(_mpb); // 3. 设置属性 // 参数1: Shader中属性的标识符ID。使用Shader.PropertyToID获取效率最高。 // 参数2: 要设置的值。 _mpb.SetColor(_Color, targetColor); // 设置主颜色 _mpb.SetFloat(_Metallic, targetMetallic); // 设置金属度 // 4. 将MPB应用到渲染器 targetRenderer.SetPropertyBlock(_mpb); } void Update() { // 动态更新示例让颜色随时间变化 if (_mpb ! null) { Color pulsatingColor Color.Lerp(Color.red, Color.blue, Mathf.Sin(Time.time) * 0.5f 0.5f); _mpb.SetColor(_Color, pulsatingColor); // 注意SetPropertyBlock需要再次调用以应用更新 targetRenderer.SetPropertyBlock(_mpb); } } }代码解析与注意事项创建与复用MaterialPropertyBlock是一个类需要实例化。绝对不要在Update循环里每帧new MaterialPropertyBlock()这会产生大量垃圾对象严重拖累性能。正确的做法是在Start或Awake中创建一次然后在整个生命周期内复用这个实例。属性名与IDSetColor、SetFloat、SetTexture等方法的第一参数可以是属性名字符串如_Color也可以是属性的Shader ID通过Shader.PropertyToID(_Color)获取。在性能关键的代码中务必使用Shader ID。因为使用字符串会导致Unity内部进行字符串查找和哈希计算而使用ID是直接的整数比较效率高得多。我们可以在类开始时缓存这些ID。SetPropertyBlock调用修改了MPB的属性后必须调用renderer.SetPropertyBlock(_mpb)才能将更改应用到渲染器。一个常见的错误是只设置了_mpb的值却忘了调用SetPropertyBlock导致修改不生效。获取现有属性renderer.GetPropertyBlock(_mpb)可以获取该渲染器当前附加的MPB。这在你想基于现有值进行修改比如让颜色更红一点时很有用。如果之前没有设置过MPB获取到的是一个空的MPB。3.2 性能优化写法缓存Shader ID让我们优化上面的代码使用缓存的Shader ID。using UnityEngine; public class OptimizedMPBExample : MonoBehaviour { public MeshRenderer targetRenderer; public Color targetColor Color.green; // 缓存Shader属性ID private static readonly int ColorShaderID Shader.PropertyToID(_Color); private static readonly int MetallicShaderID Shader.PropertyToID(_Metallic); private MaterialPropertyBlock _mpb; void Start() { _mpb new MaterialPropertyBlock(); ApplyProperties(); } void ApplyProperties() { // 使用缓存的ID进行设置效率更高 _mpb.SetColor(ColorShaderID, targetColor); _mpb.SetFloat(MetallicShaderID, 0.8f); targetRenderer.SetPropertyBlock(_mpb); } // 提供一个方法供外部修改颜色 public void ChangeColor(Color newColor) { if (_mpb null) _mpb new MaterialPropertyBlock(); // 先获取现有的避免覆盖其他可能已设置的属性 targetRenderer.GetPropertyBlock(_mpb); _mpb.SetColor(ColorShaderID, newColor); targetRenderer.SetPropertyBlock(_mpb); } }使用static readonly缓存ID是标准做法。static意味着这个ID在所有实例间共享只计算一次内存和性能开销最小。3.3 处理纹理与矢量属性MPB同样可以设置纹理和四维向量Vector4等属性。using UnityEngine; public class TextureMPBExample : MonoBehaviour { public MeshRenderer targetRenderer; public Texture2D alternateTexture; public Vector4 customData new Vector4(1.0f, 0.5f, 0.0f, 1.0f); // 例如用于Shader中的自定义参数 private static readonly int MainTexShaderID Shader.PropertyToID(_MainTex); private static readonly int CustomVectorShaderID Shader.PropertyToID(_CustomVector); private MaterialPropertyBlock _mpb; void Start() { _mpb new MaterialPropertyBlock(); // 设置替换的主纹理 if (alternateTexture ! null) { _mpb.SetTexture(MainTexShaderID, alternateTexture); } // 设置一个自定义的Vector4属性 _mpb.SetVector(CustomVectorShaderID, customData); targetRenderer.SetPropertyBlock(_mpb); } }重要提示纹理与内存通过MPB设置纹理并不会复制纹理数据。它只是传递了一个纹理的引用。因此确保你设置的纹理已经被加载到内存中例如通过Resources加载或Addressables/AssetBundle管理否则可能会引起渲染问题或性能尖峰。4. 高级应用与实战场景剖析掌握了基础我们来看看MPB在复杂项目中的实战用法。4.1 场景一大规模军队/植被的颜色差异化这是MPB最经典的场景。你需要渲染成千上万的树或士兵它们模型相同但颜色、枯萎程度、血量显示等需要有些许不同。using UnityEngine; public class ArmyColorVariation : MonoBehaviour { public GameObject soldierPrefab; public int armySize 1000; public float spawnArea 50f; // 定义几种制服颜色 private Color[] _uniformColors new Color[] { new Color(0.2f, 0.3f, 0.6f), // 深蓝 new Color(0.4f, 0.2f, 0.1f), // 棕色 new Color(0.3f, 0.5f, 0.3f), // 绿色 new Color(0.6f, 0.6f, 0.6f), // 灰色 }; private static readonly int ColorShaderID Shader.PropertyToID(_Color); void Start() { for (int i 0; i armySize; i) { Vector3 pos new Vector3( Random.Range(-spawnArea, spawnArea), 0, Random.Range(-spawnArea, spawnArea) ); GameObject soldier Instantiate(soldierPrefab, pos, Quaternion.identity, this.transform); MeshRenderer renderer soldier.GetComponentInChildrenMeshRenderer(); if (renderer ! null) { // 关键步骤每个士兵使用独立的MPB实例 // 因为每个士兵的颜色值不同不能共享同一个MPB对象。 MaterialPropertyBlock mpb new MaterialPropertyBlock(); Color randomColor _uniformColors[Random.Range(0, _uniformColors.Length)]; // 可以加入一些随机亮度变化让颜色更自然 randomColor * Random.Range(0.8f, 1.1f); mpb.SetColor(ColorShaderID, randomColor); renderer.SetPropertyBlock(mpb); } } Debug.Log($已生成 {armySize} 名颜色各异的士兵它们共享同一个材质。); } }实战心得每个Renderer需要独立的MPB对象吗是的当属性值不同时。MPB对象本身很轻量但存储了具体的属性值。如果所有士兵颜色相同可以共享同一个MPB实例。但颜色不同就必须每个Renderer有自己的MPB。不过它们仍然共享同一个底层材质资产这是内存优化的关键。合批仍然有效吗有效因为所有士兵使用相同的材质和相同的ShaderStandard并且都通过MPB设置了_Color属性即使值不同Unity仍然可以将它们动态合批显著降低Draw Call。你可以在Stats窗口里查看“Batches”数量来验证。4.2 场景二角色装备系统与纹理动态替换在RPG游戏中角色可以穿戴不同的装备。我们可以为身体基础模型准备一个材质然后通过MPB来动态替换装备部位的纹理。首先需要准备一个自定义Shader或使用支持多纹理的Shader如Standard Shader有多个贴图通道。这里假设我们有一个自定义的“角色”Shader它有_BaseTex基础颜色、_ArmorTex盔甲纹理、_EmissiveTex自发光纹理等属性。using UnityEngine; public class CharacterEquipmentSystem : MonoBehaviour { public SkinnedMeshRenderer characterRenderer; // 角色的SkinnedMeshRenderer // 装备纹理资源 public Texture2D[] armorTextures; public Texture2D[] weaponTextures; // 缓存的Shader属性ID private static readonly int ArmorTexID Shader.PropertyToID(_ArmorTex); private static readonly int WeaponTexID Shader.PropertyToID(_WeaponTex); private static readonly int EmissiveColorID Shader.PropertyToID(_EmissiveColor); private MaterialPropertyBlock _mpb; void Awake() { _mpb new MaterialPropertyBlock(); // 初始化获取当前渲染器可能已有的MPB设置比如默认装备 characterRenderer.GetPropertyBlock(_mpb); } // 更换盔甲 public void EquipArmor(int armorIndex) { if (armorIndex 0 || armorIndex armorTextures.Length) return; _mpb.SetTexture(ArmorTexID, armorTextures[armorIndex]); ApplyMPB(); Debug.Log($装备了盔甲: {armorTextures[armorIndex].name}); } // 更换武器同时可能改变自发光颜色表示附魔效果 public void EquipWeapon(int weaponIndex, bool isEnchanted) { if (weaponIndex 0 || weaponIndex weaponTextures.Length) return; _mpb.SetTexture(WeaponTexID, weaponTextures[weaponIndex]); // 动态设置其他属性比如附魔效果 Color emissiveColor isEnchanted ? Color.cyan * 2f : Color.black; _mpb.SetColor(EmissiveColorID, emissiveColor); ApplyMPB(); } // 统一应用MPB到渲染器 private void ApplyMPB() { characterRenderer.SetPropertyBlock(_mpb); } }这个方案的巨大优势内存效率无论角色有多少种装备组合内存中只有一套基础材质和若干纹理。传统方式可能需要为每种组合预生成一个材质数量是装备种类的乘积内存爆炸。动态性换装逻辑变得极其简单和高效只需替换MPB中的纹理引用无需加载或实例化新材质。易于管理纹理资源可以通过AssetBundle或Addressables异步加载然后直接赋值给MPB实现流畅的换装体验。4.3 场景三GPU Instancing与MPB的协同Unity的GPU Instancing是另一种渲染大量相同网格的优化技术。好消息是MPB可以与GPU Instancing完美配合。实际上MPB是向Instanced属性传递每实例数据的主要方式之一。要使Shader支持GPU Instancing需要在Shader中声明[PerRendererData]或使用标准的Instancing宏。Unity内置的Standard和URP/Lit Shader默认支持。using UnityEngine; public class InstancedColorWithMPB : MonoBehaviour { public Mesh mesh; public Material material; // 这个材质必须启用 GPU Instancing public int instanceCount 1000; public float area 20f; private Matrix4x4[] _matrices; private MaterialPropertyBlock _mpb; private Vector4[] _colors; private static readonly int ColorShaderID Shader.PropertyToID(_Color); void Start() { _matrices new Matrix4x4[instanceCount]; _colors new Vector4[instanceCount]; _mpb new MaterialPropertyBlock(); // 初始化每个实例的位置和颜色 for (int i 0; i instanceCount; i) { Vector3 position new Vector3( Random.Range(-area, area), Random.Range(-area, area), Random.Range(-area, area) ); Quaternion rotation Quaternion.Euler( Random.Range(0, 360), Random.Range(0, 360), Random.Range(0, 360) ); Vector3 scale Vector3.one * Random.Range(0.5f, 2f); _matrices[i] Matrix4x4.TRS(position, rotation, scale); _colors[i] new Color(Random.value, Random.value, Random.value, 1f); } // 将颜色数组设置到MPB中 // Graphics.DrawMeshInstanced 会使用这个数组为每个实例提供不同的颜色 _mpb.SetVectorArray(ColorShaderID, _colors); } void Update() { // 每帧使用同一个材质和MPB绘制所有实例 // 这是一个极高性能的绘制方式 Graphics.DrawMeshInstanced(mesh, 0, material, _matrices, instanceCount, _mpb); } }关键点Material.enableInstancing必须为true。我们使用_mpb.SetVectorArray将一个颜色数组传递进去。在绘制时GPU Instancing会为每个实例从数组中取出对应的颜色。Graphics.DrawMeshInstanced是底层渲染API它跳过了GameObject的开销直接进行网格渲染性能最高。适合用于绘制大量的环境物体如草、石头、子弹轨迹等。5. 避坑指南与性能优化最佳实践MaterialPropertyBlock用起来简单但想用得好、不出错需要注意不少细节。5.1 常见陷阱与排查技巧问题1设置了MPB但渲染没变化检查1属性名/ID是否正确。最常犯的错误是拼写错误或使用了Shader中不存在的属性名。打开Shader代码或材质的Inspector面板仔细核对。使用Shader.PropertyToID可以避免拼写错误但前提是ID对应的属性名是对的。检查2是否调用了SetPropertyBlock修改MPB后必须调用renderer.SetPropertyBlock(mpb)。检查3材质是否支持该属性如果你用的Standard Shader却尝试设置一个自定义属性_MyCustomValue那肯定是无效的。MPB只能覆盖Shader中已定义的属性。检查4MPB的作用范围。MPB是附加到Renderer组件上的。如果你有多个子MeshRenderer比如一个角色由多个部分组成的SkinnedMeshRenderer你需要为每个部分单独设置MPB或者使用GetComponentsInChildrenRenderer来批量设置。问题2使用了MPB后物体的阴影看起来不对原因MPB修改的是材质属性但阴影投射Shadow Casting有时会使用一个简化的Shadow Pass或Fallback Shader。如果这个Shadow Pass没有使用你通过MPB覆盖的属性比如_Color的Alpha通道用于透明度阴影那么阴影就不会受到影响。解决方案确保你的Shader的Shadow Pass能够正确处理MPB传递的属性。对于Standard Shader通常没问题。对于自定义Shader你可能需要在Shadow Pass中也添加对[PerRendererData]属性的支持。问题3在编辑模式下MPB的修改不可见原因这是设计使然。MPB是运行时数据不会序列化到场景或预制体中。你只能在Play模式下看到效果。变通方案如果需要编辑模式预览可以写一个Editor脚本在OnInspectorGUI中模拟MPB的效果或者考虑使用ScriptableObject来存储配置在运行时通过MPB应用。问题4MPB导致合批失败排查在Game视图的Stats面板中观察Batches数量。如果使用了MPB后Batches反而增加了可能是以下原因Renderer的Lightmap Index或Light Probe Usage不同即使材质和MPB相同光照贴图或光照探针设置不同也会打断合批。Shader中使用了MaterialPropertyBlock不支持的属性例如通过Material.EnableKeyword开启的Shader变体Keywords无法通过MPB设置。不同的Keyword会导致Shader变体不同从而打断合批。你需要确保所有使用MPB的物体其材质的全局Shader Keywords是一致的。MPB设置的结构不同物体A通过MPB设置了_Color和_Metallic物体B只设置了_Color。虽然值可能相同但MPB“覆盖的属性集合”不同也可能影响合批。尽量保持所有物体通过MPB设置的属性列表一致。5.2 性能优化最佳实践清单永远缓存Shader.PropertyToID这是第一条也是最重要的一条。在静态字段或类构造函数中缓存所有需要用到的属性ID。复用MaterialPropertyBlock对象在类级别声明一个私有字段在Awake或Start中初始化。避免在任何循环或每帧逻辑中new MaterialPropertyBlock()。谨慎使用GetPropertyBlock这个方法会从GPU回读数据有一定开销。如果你不需要之前的属性值直接创建新的MPB并设置即可。如果需要基于现有值修改才使用它。批量设置属性尽量在一次SetPropertyBlock调用前完成所有属性的设置SetColor,SetFloat,SetTexture等而不是设置一个属性就调用一次SetPropertyBlock。理解合批条件MPB是为合批服务的。确保你的物体除了MPB属性值不同外其他影响合批的因素材质、Shader Keywords、Lightmap Index等都保持一致。与GPU Instancing结合对于海量相同物体优先考虑使用Graphics.DrawMeshInstanced配合MPB的SetVectorArray等方法这是性能最高的渲染方式。清理MPB当一个物体不再需要MPB时例如被回收进对象池调用renderer.SetPropertyBlock(null)来清除附加的MPB避免残留设置影响后续复用。在URP/HDRP中的使用在SRP可编程渲染管线如URP/HDRP中MPB同样适用。但需要注意URP Shader的属性名可能有所不同如_BaseColor代替了_Color。查看Shader的源码或使用Frame Debugger来确认正确的属性名。5.3 一个完整的、生产可用的MPB管理器示例最后分享一个我项目中常用的简单MPB管理器用于管理大量需要动态颜色的物体。using System.Collections.Generic; using UnityEngine; public class MPBColorManager : MonoBehaviour { public static MPBColorManager Instance { get; private set; } // 预定义的颜色方案 public Color[] teamColors; // 缓存Renderer到其颜色ID和MPB的映射 private DictionaryRenderer, (int colorId, MaterialPropertyBlock mpb) _rendererData new DictionaryRenderer, (int, MaterialPropertyBlock)(); private static readonly int ColorShaderID Shader.PropertyToID(_Color); void Awake() { if (Instance ! null Instance ! this) { Destroy(this); return; } Instance this; } // 注册一个Renderer并赋予其初始颜色 public void RegisterRenderer(Renderer renderer, int initialColorIndex 0) { if (renderer null || _rendererData.ContainsKey(renderer)) return; initialColorIndex Mathf.Clamp(initialColorIndex, 0, teamColors.Length - 1); MaterialPropertyBlock mpb new MaterialPropertyBlock(); mpb.SetColor(ColorShaderID, teamColors[initialColorIndex]); renderer.SetPropertyBlock(mpb); _rendererData.Add(renderer, (initialColorIndex, mpb)); } // 改变一个Renderer的颜色 public void ChangeRendererColor(Renderer renderer, int newColorIndex) { if (!_rendererData.TryGetValue(renderer, out var data)) return; newColorIndex Mathf.Clamp(newColorIndex, 0, teamColors.Length - 1); data.mpb.SetColor(ColorShaderID, teamColors[newColorIndex]); renderer.SetPropertyBlock(data.mpb); // 更新缓存 _rendererData[renderer] (newColorIndex, data.mpb); } // 批量改变颜色例如整个队伍变色 public void ChangeColorForAll(int colorIndex) { Color targetColor teamColors[Mathf.Clamp(colorIndex, 0, teamColors.Length - 1)]; foreach (var kvp in _rendererData) { kvp.Value.mpb.SetColor(ColorShaderID, targetColor); kvp.Key.SetPropertyBlock(kvp.Value.mpb); // 注意这里直接修改了元组内的mpb但元组是值类型所以需要更新字典 // 实际上mpb是引用类型我们修改的是同一个对象所以不需要更新字典。 // 但需要更新colorId。为了简化这里只更新颜色值不更新缓存中的colorId。 // 更严谨的做法是创建一个可变的类来存储数据。 } } // 注销Renderer物体被销毁时调用 public void UnregisterRenderer(Renderer renderer) { if (renderer ! null) { _rendererData.Remove(renderer); // 可选清除MPB // renderer.SetPropertyBlock(null); } } void OnDestroy() { // 清理所有MPB引用 foreach (var renderer in _rendererData.Keys) { if (renderer ! null) { renderer.SetPropertyBlock(null); } } _rendererData.Clear(); } }这个管理器提供了集中式的颜色控制避免了每个物体自己创建和管理MPB减少了代码冗余和潜在的错误。你可以根据需要扩展它支持更多的属性如纹理、浮点数和更复杂的管理逻辑。MaterialPropertyBlock是一个强大的工具它完美体现了“将变化的数据与不变的资源分离”的优化思想。在性能敏感的项目中善用MPB往往能以很小的代价换来显著的帧率提升和内存节省。希望这篇实战指南能帮你彻底掌握它并在你的下一个Unity项目中游刃有余地应用起来。