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中创建一次,然后在整个生命周期内复用这个实例。 - 属性名与ID:
SetColor、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.GetComponentInChildren<MeshRenderer>(); 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。不过,它们仍然共享同一个底层材质资产,这是内存优化的关键。
- 合批仍然有效吗?有效!因为所有士兵使用相同的材质和相同的Shader(Standard),并且都通过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中已定义的属性。 - 检查4:MPB的作用范围。MPB是附加到
Renderer组件上的。如果你有多个子MeshRenderer(比如一个角色由多个部分组成的SkinnedMeshRenderer),你需要为每个部分单独设置MPB,或者使用GetComponentsInChildren<Renderer>来批量设置。
问题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应用。
问题4:MPB导致合批失败?
- 排查:在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 Dictionary<Renderer, (int colorId, MaterialPropertyBlock mpb)> _rendererData = new Dictionary<Renderer, (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项目中游刃有余地应用起来。
