Unity SpriteRenderer 进度条 Shader 实现

快速索引

问题 解决方案 关键技术
如何选择合适的方案? 根据贴图类型选择 独立贴图/图集适配
图集中的Sprite进度异常 UV坐标重映射 _MainTex_ST参数
访问material自动克隆 使用MaterialPropertyBlock SetPropertyBlock()
多个进度条性能差 共享材质+MPB 避免材质实例化
Shader性能优化 减少计算、使用内置函数 step(), lerp()

一、方案选择指南 ⚡

1.1 快速决策表

场景 推荐方案 原因
单个进度条、独立贴图 方案A(独立版) 简单直接,开发快
多个进度条、使用图集 方案B(图集版) 避免UV错误
UGUI + Sprite Atlas 方案B(图集版) 必须支持图集
原型开发、快速验证 方案A(独立版) 代码简单
生产环境、大量实例 方案B(图集版) 性能优、通用性强

1.2 判断流程图

复制代码
flowchart TD
    A[需要进度条Shader] --> B{Sprite在图集中?}
    B -->|是| C[方案B: 图集适配版]
    B -->|否| D{会打包成图集?}
    D -->|是| C
    D -->|否| E[方案A: 独立贴图版]
    E --> F{后续可能用图集?}
    F -->|是| G[建议直接用方案B<br/>避免后期修改]
    F -->|否| H[方案A即可<br/>代码更简单]

1.3 两种方案对比

对比项 方案A:独立贴图版 方案B:图集适配版
适用场景 Sprite独立存储 Sprite打包在图集中
代码复杂度 ⭐⭐ 简单 ⭐⭐⭐ 稍复杂
性能开销 ⭐⭐⭐⭐⭐ 最优 ⭐⭐⭐⭐ 略高
通用性 ⭐⭐ 仅独立贴图 ⭐⭐⭐⭐⭐ 通用
学习价值 ⭐⭐⭐ 适合入门 ⭐⭐⭐⭐⭐ 理解图集机制
维护成本 ⭐⭐⭐⭐⭐ 低 ⭐⭐⭐⭐ 稍高

推荐建议:

  • 🎓 学习阶段:从方案A开始,理解基础原理
  • 🚀 生产项目:直接使用方案B,避免后期修改
  • 快速原型:使用方案A,后续可升级到方案B

二、方案A:独立贴图版(基础实现)🚀

2.1 适用场景

推荐使用场景:

  • Sprite独立存储(未打包图集)
  • 快速原型开发
  • 学习Shader基础知识
  • 确定不会使用Sprite Atlas

⚠️ 不适用场景:

  • Sprite在图集中
  • 使用UGUI的Sprite Atlas
  • 可能后续打包成图集

2.2 核心原理

使用Shader的clip()函数,根据UV坐标实现横向/纵向进度条效果:

💡 clip()函数说明:当参数<0时丢弃该像素(不渲染),用于实现裁剪效果,是GPU硬件级优化函数。

复制代码
进度值 = 0.5  →  显示左半部分,裁剪右半部分
UV.x < 0.5    →  保留
UV.x >= 0.5   →  裁剪(discard)

关键点: 直接使用texcoord坐标,因为独立贴图的UV范围就是0-1。

2.3 完整Shader代码

文件位置:unity/ProgressBarCutoff.shader

hlsl 复制代码
Shader "Custom/ProgressBarCutoff"
{
    Properties
    {
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 1
        _FillDirection ("Fill Direction", Float) = 0 // 0=水平, 1=垂直
        
        [Header(Segment Dividers)]
        _DividerTex ("Divider Texture", 2D) = "white" {}
        _SegmentCount ("Segment Count", Int) = 1
        _DividerWidth ("Divider Width", Range(0, 0.1)) = 0.02
        _DividerColor ("Divider Color", Color) = (1,1,1,1)
        
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_local _ PIXELSNAP_ON
            
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _DividerTex;
            fixed4 _Color;
            fixed4 _DividerColor;
            float _FillAmount;
            float _FillDirection;
            int _SegmentCount;
            float _DividerWidth;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex);
                OUT.color = IN.color * _Color;
                
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap(OUT.vertex);
                #endif

                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                float halfDividerWidth = _DividerWidth * 0.5;
                float isVertical = step(0.5, _FillDirection);
                
                // ⭐ 关键:直接使用texcoord(适用于独立贴图)
                float coord = lerp(IN.texcoord.x, IN.texcoord.y, isVertical);
                
                // 裁剪超出进度的部分
                clip(_FillAmount - coord);
                
                fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
                float segmentMask = step(1.5, _SegmentCount);
                float segmentWidth = 1.0 / max(_SegmentCount, 1);
                float normalizedPos = fmod(coord, segmentWidth);
                float distToStart = normalizedPos;
                float segmentIndex = floor(coord / segmentWidth);
                float dividerMask = step(distToStart, halfDividerWidth) * step(0.5, segmentIndex);

                
                float2 dividerUV;
                dividerUV.x = lerp(normalizedPos / _DividerWidth, IN.texcoord.x, isVertical);
                dividerUV.y = lerp(IN.texcoord.y, normalizedPos / _DividerWidth, isVertical);
                
                fixed4 divider = tex2D(_DividerTex, dividerUV) * _DividerColor;
                float finalMask = segmentMask * dividerMask;
                
                c = lerp(c, divider, divider.a * finalMask);
                c.rgb *= c.a;
                return c;
            }
            ENDCG
        }
    }
}

2.4 核心代码解析

关键Fragment Shader部分:

hlsl 复制代码
fixed4 frag(v2f IN) : SV_Target
{
    // 1. 判断填充方向(0=水平,1=垂直)
    float isVertical = step(0.5, _FillDirection);
    
    // 2. ⭐ 核心:直接使用texcoord(独立贴图的UV就是0-1)
    float coord = lerp(IN.texcoord.x, IN.texcoord.y, isVertical);
    
    // 3. 裁剪超出进度的部分
    clip(_FillAmount - coord);
    
    // 4. 采样并返回颜色
    fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
    // ... 分段线逻辑
    return c;
}

为什么可以直接使用texcoord

  • 独立贴图的UV坐标范围就是(0,0)到(1,1)
  • 不需要任何重映射计算
  • 性能最优,代码最简单

2.5 C#使用示例

💡 MaterialPropertyBlock说明:用于修改渲染器属性而不创建新材质实例,多个对象可共享同一材质,避免性能开销。

csharp 复制代码
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class ProgressBarStandalone : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    private MaterialPropertyBlock mpb;
    
    [Header("进度设置")]
    [Range(0, 1)]
    [SerializeField] private float progress = 1f;
    
    // Shader参数ID(性能优化)
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    
    void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        mpb = new MaterialPropertyBlock();
    }
    
    void Start()
    {
        // ✅ 确保使用的是独立贴图方案的Shader
        if (spriteRenderer.sharedMaterial.shader.name != "Custom/ProgressBarCutoff")
        {
            Debug.LogWarning("请确保使用ProgressBarCutoff shader!");
        }
    }
    
    public void SetProgress(float value)
    {
        progress = Mathf.Clamp01(value);
        
        // ✅ 使用MaterialPropertyBlock避免材质克隆
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FillAmountID, progress);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

2.6 优点与限制

优点:

  • 代码简单,易于理解和维护
  • 性能开销最小(无UV重映射计算)
  • 适合快速原型开发
  • 学习Shader的最佳起点

限制:

  • 仅支持独立贴图
  • 不支持Sprite Atlas
  • 如果后期需要图集,需要切换到方案B

三、方案B:图集适配版(生产级)⭐

3.1 适用场景

推荐使用场景:

  • Sprite打包在图集中(Sprite Atlas)
  • UGUI项目使用图集
  • 生产环境多个进度条实例
  • 需要减少DrawCall和内存占用

通用性:

  • 兼容独立贴图(向下兼容)
  • 兼容图集贴图
  • 一次编写,处处使用

3.2 图集问题详解

问题产生原因

当Sprite位于图集(Sprite Atlas)中时:

  1. spriteRenderer.sprite 显示正常(Unity自动处理UV)
  2. spriteRenderer.material.mainTexture 变成整个图集贴图
  3. Shader中的UV坐标基于整个图集,不是单个Sprite的0-1范围

示意图:

复制代码
独立贴图的UV:           图集中Sprite的UV:
┌─────────────┐         ┌─────────────────────────┐
│  (0,1)  (1,1) │         │                         │
│             │         │  (0.1,0.7) (0.3,0.7)     │
│   Sprite    │         │  ┌──────┐               │
│             │         │  │Sprite│  ← 实际UV只占  │
│  (0,0)  (1,0) │         │  └──────┘     一小部分  │
└─────────────┘         │  (0.1,0.5) (0.3,0.5)     │
UV范围: 0-1              │                         │
                        └─────────────────────────┘
                        UV范围: 0.1-0.3, 0.5-0.7
错误示例分析
hlsl 复制代码
// ❌ 错误:直接使用UV会基于整个图集计算
fixed4 frag (v2f i) : SV_Target
{
    // 假设Sprite在图集中的UV是 (0.1, 0.5) 到 (0.3, 0.7)
    // i.uv.x 的范围是 0.1~0.3,而非 0~1
    // 当_FillAmount=0.5时,会错误裁剪!
    clip(_FillAmount - i.uv.x);  // ❌ 错误!
    
    return tex2D(_MainTex, i.uv);
}

错误结果:

  • 进度条区域不对
  • 或者整个图集都在变化
  • 进度值与实际显示不符

3.3 解决方案:UV重映射

_MainTex_ST参数详解

💡 _MainTex_ST含义:ST代表Scale & Translation(缩放与平移),Unity自动提供用于贴图变换的四维向量。

Unity自动为每个贴图提供_MainTex_ST参数:

hlsl 复制代码
float4 _MainTex_ST;
// .xy = Tiling(缩放)  // Sprite在图集中的尺寸比例
// .zw = Offset(偏移)  // Sprite在图集中的位置偏移

重映射公式:

hlsl 复制代码
float2 spriteUV = (atlasUV - _MainTex_ST.zw) / _MainTex_ST.xy;

举例说明:

复制代码
假设Sprite在图集中:
- 位置偏移(Offset): (0.1, 0.5)
- 尺寸比例(Tiling): (0.2, 0.2)  // 占图集的20%

图集UV: (0.15, 0.6)
重映射后:
spriteUV = ((0.15, 0.6) - (0.1, 0.5)) / (0.2, 0.2)
         = (0.05, 0.1) / (0.2, 0.2)
         = (0.25, 0.5)

✅ 现在spriteUV是相对于Sprite的0-1范围!

3.4 完整Shader代码(图集适配版)

创建新文件:ProgressBarAtlas.shader

hlsl 复制代码
Shader "Custom/ProgressBarAtlas"
{
    Properties
    {
        _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _FillAmount ("Fill Amount", Range(0, 1)) = 1
        _FillDirection ("Fill Direction", Float) = 0 // 0=水平, 1=垂直
        
        [Header(Segment Dividers)]
        _DividerTex ("Divider Texture", 2D) = "white" {}
        _SegmentCount ("Segment Count", Int) = 1
        _DividerWidth ("Divider Width", Range(0, 0.1)) = 0.02
        _DividerColor ("Divider Color", Color) = (1,1,1,1)
        
        [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile_local _ PIXELSNAP_ON
            
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;  // ⭐ 关键:用于UV重映射
            sampler2D _DividerTex;
            fixed4 _Color;
            fixed4 _DividerColor;
            float _FillAmount;
            float _FillDirection;
            int _SegmentCount;
            float _DividerWidth;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex);
                OUT.color = IN.color * _Color;
                
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap(OUT.vertex);
                #endif

                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                // ⭐⭐⭐ 核心差异:UV重映射
                // 将图集UV转换为Sprite的0-1范围
                float2 spriteUV = (IN.texcoord - _MainTex_ST.zw) / _MainTex_ST.xy;
                
                float halfDividerWidth = _DividerWidth * 0.5;
                float isVertical = step(0.5, _FillDirection);
                
                // 使用重映射后的UV计算进度
                float coord = lerp(spriteUV.x, spriteUV.y, isVertical);
                
                // 裁剪超出进度的部分
                clip(_FillAmount - coord);
                
                // 采样时使用原始UV(图集UV)
                fixed4 c = tex2D(_MainTex, IN.texcoord) * IN.color;
                
                // 分段线计算(使用重映射后的UV)
                float segmentMask = step(1.5, _SegmentCount);
                float segmentWidth = 1.0 / max(_SegmentCount, 1);
                float normalizedPos = fmod(coord, segmentWidth);
                float distToStart = normalizedPos;
                float segmentIndex = floor(coord / segmentWidth);
                float dividerMask = step(distToStart, halfDividerWidth) * step(0.5, segmentIndex);

                float2 dividerUV;
                dividerUV.x = lerp(normalizedPos / _DividerWidth, spriteUV.x, isVertical);
                dividerUV.y = lerp(spriteUV.y, normalizedPos / _DividerWidth, isVertical);
                
                fixed4 divider = tex2D(_DividerTex, dividerUV) * _DividerColor;
                float finalMask = segmentMask * dividerMask;
                
                c = lerp(c, divider, divider.a * finalMask);
                c.rgb *= c.a;
                return c;
            }
            ENDCG
        }
    }
}

3.5 核心代码对比

步骤 独立贴图版 图集适配版
UV获取 float coord = IN.texcoord.x; float2 spriteUV = (IN.texcoord - _MainTex_ST.zw) / _MainTex_ST.xy;
进度计算 直接使用texcoord 使用重映射后的spriteUV
纹理采样 tex2D(_MainTex, IN.texcoord) tex2D(_MainTex, IN.texcoord) ⚠️ 仍用原始UV
性能 ⭐⭐⭐⭐⭐ 最优 ⭐⭐⭐⭐ 稍低(多1次除法)

关键注意:

  • ✅ 进度计算使用spriteUV(重映射后的0-1范围)
  • ✅ 纹理采样使用IN.texcoord(原始图集UV)

3.6 C#使用示例

csharp 复制代码
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class ProgressBarAtlas : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    private MaterialPropertyBlock mpb;
    
    [Header("进度设置")]
    [Range(0, 1)]
    [SerializeField] private float progress = 1f;
    
    [Header("动画设置")]
    public bool smoothProgress = true;
    public float progressSpeed = 2f;
    
    private float targetProgress;
    private float currentProgress;
    
    // Shader参数ID(性能优化)
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    private static readonly int ColorID = Shader.PropertyToID("_Color");
    
    void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        mpb = new MaterialPropertyBlock();
        
        currentProgress = progress;
        targetProgress = progress;
    }
    
    void Start()
    {
        // ✅ 确保使用图集适配版Shader
        if (spriteRenderer.sharedMaterial.shader.name != "Custom/ProgressBarAtlas")
        {
            Debug.LogWarning("建议使用ProgressBarAtlas shader以支持图集!");
        }
    }
    
    void Update()
    {
        if (smoothProgress && !Mathf.Approximately(currentProgress, targetProgress))
        {
            currentProgress = Mathf.Lerp(currentProgress, targetProgress, 
                Time.deltaTime * progressSpeed);
            
            if (Mathf.Abs(currentProgress - targetProgress) < 0.01f)
                currentProgress = targetProgress;
            
            ApplyProgress();
        }
    }
    
    public void SetProgress(float value)
    {
        targetProgress = Mathf.Clamp01(value);
        
        if (!smoothProgress)
        {
            currentProgress = targetProgress;
            ApplyProgress();
        }
    }
    
    public void SetProgressImmediate(float value)
    {
        currentProgress = Mathf.Clamp01(value);
        targetProgress = currentProgress;
        ApplyProgress();
    }
    
    public void SetColor(Color color)
    {
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetColor(ColorID, color);
        spriteRenderer.SetPropertyBlock(mpb);
    }
    
    private void ApplyProgress()
    {
        // ✅ 使用MaterialPropertyBlock避免材质克隆
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FillAmountID, currentProgress);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

3.7 优点与适用性

优点:

  • 完全支持Sprite Atlas
  • 兼容独立贴图(向下兼容)
  • 适合生产环境
  • 一次编写,处处使用

⚠️ 性能考虑:

  • 比独立版多1次UV重映射计算(除法运算)
  • 对于现代GPU,性能影响可忽略不计
  • 换来的是更好的通用性和维护性

四、MaterialPropertyBlock最佳实践 ⭐

无论使用方案A还是方案B,都必须使用MaterialPropertyBlock!

4.1 Material克隆问题

问题描述

访问spriteRenderer.material自动克隆材质球,导致:

  1. 内存泄漏:每次访问创建新实例
  2. DrawCall爆炸:无法批处理
  3. 性能下降:100个进度条产生100个材质实例
错误示例
csharp 复制代码
// ❌ 错误:每次调用都会克隆材质!
void UpdateProgress(float value)
{
    spriteRenderer.material.SetFloat("_FillAmount", value); // ❌ 自动克隆!
}

// 结果:
// 调用1次 → 克隆1个材质实例
// 调用100次 → 克隆100个材质实例 → 内存泄漏!
检测克隆
csharp 复制代码
void Start()
{
    Debug.Log($"Shared Material: {spriteRenderer.sharedMaterial.name}"); 
    // 输出: "ProgressMaterial"
    
    // 第一次访问material
    var mat = spriteRenderer.material;
    Debug.Log($"Cloned Material: {mat.name}"); 
    // 输出: "ProgressMaterial (Instance)" ← 注意后缀!
}

4.2 正确解决方案

csharp 复制代码
public class ProgressBarOptimized : MonoBehaviour
{
    private SpriteRenderer spriteRenderer;
    private MaterialPropertyBlock mpb; // ✅ 使用MPB
    
    // ✅ 缓存Shader参数ID(性能优化)
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    
    void Start()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        mpb = new MaterialPropertyBlock();
    }
    
    public void UpdateProgress(float value)
    {
        // ✅ 正确:不会克隆材质,支持批处理
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FillAmountID, value);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

4.3 性能对比

方法 100个进度条 说明
Draw Call
material.SetFloat() 100个 ❌ 每个对象一个DC
sharedMaterial.SetFloat() 1个 ⚠️ 全局修改所有实例
MaterialPropertyBlock 1个 ✅ 最优!
材质实例数
material.SetFloat() 100个 ❌ 克隆100个实例
sharedMaterial.SetFloat() 1个 ⚠️ 所有对象共享
MaterialPropertyBlock 1个 ✅ 共享材质+独立参数
FPS影响
material.SetFloat() 30-40 FPS ❌ 性能差
MaterialPropertyBlock 60 FPS ✅ 性能优

4.4 MaterialPropertyBlock优势总结

优势 说明
不克隆材质 所有对象共享同一材质
保持批处理 1个DrawCall渲染多个对象
独立参数 每个对象可以有不同的进度值
性能最优 没有材质实例化开销
内存友好 不会产生材质实例泄漏

五、高级用法与最佳实践

5.1 动态创建进度条

csharp 复制代码
using UnityEngine;

public class ProgressBarSpawner : MonoBehaviour
{
    public Sprite progressSprite; // 可以来自图集
    public Material progressMaterial; // 使用对应的Shader材质
    
    public GameObject CreateProgressBar(Vector3 position, bool useAtlas = true)
    {
        GameObject go = new GameObject("ProgressBar");
        go.transform.position = position;
        
        SpriteRenderer sr = go.AddComponent<SpriteRenderer>();
        sr.sprite = progressSprite;
        sr.sharedMaterial = progressMaterial; // ✅ 使用sharedMaterial
        
        // 根据是否使用图集选择不同的组件
        if (useAtlas)
        {
            var bar = go.AddComponent<ProgressBarAtlas>();
            bar.SetProgress(1f);
        }
        else
        {
            var bar = go.AddComponent<ProgressBarStandalone>();
            bar.SetProgress(1f);
        }
        
        return go;
    }
}

5.2 进度条对象池

csharp 复制代码
using UnityEngine;
using System.Collections.Generic;

public class ProgressBarPool : MonoBehaviour
{
    public GameObject progressBarPrefab;
    public int poolSize = 20;
    
    private Queue<GameObject> pool = new Queue<GameObject>();
    
    void Start()
    {
        // 预创建对象池
        for (int i = 0; i < poolSize; i++)
        {
            CreateNewBar();
        }
    }
    
    GameObject CreateNewBar()
    {
        var bar = Instantiate(progressBarPrefab, transform);
        bar.SetActive(false);
        pool.Enqueue(bar);
        return bar;
    }
    
    public GameObject GetProgressBar()
    {
        if (pool.Count == 0)
            CreateNewBar();
        
        var bar = pool.Dequeue();
        bar.SetActive(true);
        return bar;
    }
    
    public void ReturnProgressBar(GameObject bar)
    {
        bar.SetActive(false);
        pool.Enqueue(bar);
    }
}

5.3 Shader性能优化技巧

减少分支判断
hlsl 复制代码
// ❌ 低效:多次if判断
if (spriteUV.x > _FillAmount)
    discard;
if (spriteUV.y < 0 || spriteUV.y > 1)
    discard;

// ✅ 高效:使用clip()一次判断
clip(_FillAmount - spriteUV.x);
使用内置函数
hlsl 复制代码
// ❌ 低效:三元运算符
float alpha = spriteUV.x < _FillAmount ? 1.0 : 0.0;

// ✅ 高效:使用step()
float alpha = step(spriteUV.x, _FillAmount);

// ✅ 更高效:使用lerp()组合
float isVertical = step(0.5, _FillDirection);
float coord = lerp(spriteUV.x, spriteUV.y, isVertical);
缓存Shader参数ID
csharp 复制代码
public class ProgressBarOptimized : MonoBehaviour
{
    // ✅ 静态缓存,所有实例共享
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    private static readonly int ColorID = Shader.PropertyToID("_Color");
    private static readonly int FillDirectionID = Shader.PropertyToID("_FillDirection");
    
    public void SetProgress(float value)
    {
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FillAmountID, value); // 使用缓存的ID
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

六、常见问题与解决方案

6.1 进度条显示异常

现象: 进度条区域不对,或整个图集都在变化

原因分析:

  1. Sprite在图集中,但使用了独立贴图版Shader
  2. UV未重映射

解决方案:

csharp 复制代码
// 方案1:切换到图集适配版Shader
spriteRenderer.sharedMaterial = atlasShaderMaterial;

// 方案2:确保Shader中有UV重映射
// 检查fragment shader中是否有:
// float2 spriteUV = (IN.texcoord - _MainTex_ST.zw) / _MainTex_ST.xy;

6.2 Draw Call过高

现象: 100个进度条产生100个Draw Call

原因: 使用了material而非MaterialPropertyBlock

错误代码:

csharp 复制代码
// ❌ 错误
void UpdateProgress(float value)
{
    spriteRenderer.material.SetFloat("_FillAmount", value); // ❌ 克隆材质!
}

正确代码:

csharp 复制代码
// ✅ 正确
private MaterialPropertyBlock mpb;

void Start()
{
    mpb = new MaterialPropertyBlock();
}

void UpdateProgress(float value)
{
    spriteRenderer.GetPropertyBlock(mpb);
    mpb.SetFloat("_FillAmount", value);
    spriteRenderer.SetPropertyBlock(mpb);
}

6.3 材质实例泄漏

现象: 运行一段时间后内存持续增长

原因: 访问material产生的克隆未销毁

解决方案:

csharp 复制代码
public class ProgressBar : MonoBehaviour
{
    private Material clonedMaterial; // 如果确实需要克隆材质
    
    void Start()
    {
        // 如果必须要克隆材质(不推荐)
        clonedMaterial = new Material(spriteRenderer.sharedMaterial);
        spriteRenderer.material = clonedMaterial;
    }
    
    void OnDestroy()
    {
        // ⚠️ 必须手动销毁!
        if (Application.isPlaying && clonedMaterial != null)
        {
            Destroy(clonedMaterial);
        }
    }
}

// ✅ 但强烈建议:直接用MaterialPropertyBlock,避免这个问题!

6.4 图集边缘出现白边

原因: UV超出Sprite范围,采样到相邻图片

解决方案:

hlsl 复制代码
// 在fragment shader中限制UV范围
float2 spriteUV = (IN.texcoord - _MainTex_ST.zw) / _MainTex_ST.xy;
spriteUV = clamp(spriteUV, 0.001, 0.999); // 稍微收缩避免边缘采样问题

6.5 如何选择方案

决策流程:

csharp 复制代码
public class ProgressBarManager : MonoBehaviour
{
    public Material standaloneShader; // 独立贴图版
    public Material atlasShader;      // 图集适配版
    
    public Material GetAppropriateMaterial(Sprite sprite)
    {
        // 检查Sprite是否在图集中
        if (sprite.packed)
        {
            Debug.Log("Sprite在图集中,使用图集适配版");
            return atlasShader;
        }
        else
        {
            Debug.Log("独立Sprite,可使用独立版或图集版");
            return atlasShader; // 建议统一使用图集版(向下兼容)
        }
    }
}

建议:

  • 🎓 学习阶段:两个版本都尝试,理解差异
  • 🚀 生产项目:统一使用图集适配版(方案B)
  • ⚡ 性能敏感:如果确定不用图集,可用独立版(方案A)

七、性能测试

7.1 测试场景

csharp 复制代码
using UnityEngine;

public class ProgressBarBenchmark : MonoBehaviour
{
    public int barCount = 100;
    public GameObject barPrefab;
    public bool useAtlas = true;
    
    void Start()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        
        for (int i = 0; i < barCount; i++)
        {
            var bar = Instantiate(barPrefab);
            bar.transform.position = new Vector3(
                Random.Range(-10, 10),
                Random.Range(-10, 10),
                0
            );
            
            // 设置随机进度
            var pb = bar.GetComponent<ProgressBarAtlas>();
            if (pb != null)
            {
                pb.SetProgress(Random.value);
            }
        }
        
        sw.Stop();
        Debug.Log($"Created {barCount} progress bars in {sw.ElapsedMilliseconds}ms");
        Debug.Log($"Using Atlas Shader: {useAtlas}");
    }
}

7.2 性能指标

测试项 独立版 图集版 说明
100个进度条(独立贴图)
Draw Call 1-2 1-2 ✅ 相同
FPS 60 59-60 ⚡ 图集版略低(可忽略)
内存占用 正常 正常 ✅ 相同
100个进度条(图集贴图)
Draw Call ❌ 错误显示 1-2 ✅ 图集版正常
FPS ❌ N/A 60 ✅ 图集版正常
性能结论
UV重映射开销 ~1-2% 现代GPU可忽略
通用性 仅独立贴图 通用 图集版推荐

7.3 最佳实践建议

csharp 复制代码
// ✅ 推荐的实现检查清单
public class ProgressBarChecklist : MonoBehaviour
{
    // ✓ 使用MaterialPropertyBlock
    private MaterialPropertyBlock mpb;
    
    // ✓ 缓存组件引用
    private SpriteRenderer spriteRenderer;
    
    // ✓ 缓存Shader参数ID
    private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");
    
    // ✓ 使用图集适配版Shader(或根据情况选择)
    // 检查Shader: "Custom/ProgressBarAtlas" 或 "Custom/ProgressBarCutoff"
    
    // ✓ 使用sharedMaterial而非material
    void Start()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        mpb = new MaterialPropertyBlock();
        
        Debug.Log($"Using material: {spriteRenderer.sharedMaterial.name}");
    }
    
    // ✓ 性能优化:仅在需要时更新
    public void SetProgress(float value)
    {
        spriteRenderer.GetPropertyBlock(mpb);
        mpb.SetFloat(FillAmountID, value);
        spriteRenderer.SetPropertyBlock(mpb);
    }
}

八、总结

8.1 核心要点

方案选择
场景 推荐方案 原因
学习Shader 方案A(独立版) 代码简单,易理解
生产项目 方案B(图集版) 通用性强,避免后期修改
确定不用图集 方案A(独立版) 性能稍优
使用Sprite Atlas 方案B(图集版) 必须
关键技术对比
技术点 独立贴图版 图集适配版
UV处理 直接用texcoord 重映射spriteUV
核心公式 coord = texcoord.x spriteUV = (texcoord - offset) / tiling
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
通用性 ⭐⭐ ⭐⭐⭐⭐⭐
MaterialPropertyBlock
csharp 复制代码
// ✅ 必备最佳实践(两个方案都适用)
private MaterialPropertyBlock mpb;
private static readonly int FillAmountID = Shader.PropertyToID("_FillAmount");

void UpdateProgress(float value)
{
    spriteRenderer.GetPropertyBlock(mpb);
    mpb.SetFloat(FillAmountID, value);
    spriteRenderer.SetPropertyBlock(mpb);
}

8.2 完整文件清单

复制代码
项目结构:
├── Shaders/
│   ├── ProgressBarCutoff.shader      ← 方案A:独立贴图版
│   └── ProgressBarAtlas.shader       ← 方案B:图集适配版
├── Materials/
│   ├── ProgressMaterial_Standalone.mat  ← 使用ProgressBarCutoff
│   └── ProgressMaterial_Atlas.mat       ← 使用ProgressBarAtlas
└── Scripts/
    ├── ProgressBarStandalone.cs      ← 独立版组件
    ├── ProgressBarAtlas.cs           ← 图集版组件
    └── ProgressBarPool.cs            ← 对象池管理

8.3 学习路线图

复制代码
1. 基础理解(方案A)
   └─ 学习clip()裁剪机制
   └─ 理解UV坐标系统
   └─ 掌握MaterialPropertyBlock

2. 进阶应用(方案B)
   └─ 理解图集UV映射
   └─ 掌握_MainTex_ST参数
   └─ 实现UV重映射计算

3. 性能优化
   └─ 使用对象池
   └─ 缓存Shader参数ID
   └─ 减少Shader计算

4. 生产应用
   └─ 根据项目需求选择方案
   └─ 实现进度条管理系统
   └─ 性能测试与优化

8.4 关键公式总结

图集UV重映射公式:

hlsl 复制代码
float2 spriteUV = (atlasUV - _MainTex_ST.zw) / _MainTex_ST.xy;
// atlasUV: 图集中的UV坐标
// _MainTex_ST.xy: Tiling(Sprite尺寸比例)
// _MainTex_ST.zw: Offset(Sprite位置偏移)
// spriteUV: 重映射后的0-1范围UV

进度裁剪判断:

hlsl 复制代码
float coord = lerp(spriteUV.x, spriteUV.y, isVertical);
clip(_FillAmount - coord);
// _FillAmount > coord: 保留
// _FillAmount <= coord: 裁剪(discard)

通过本教程,你应该掌握:

  1. 两种实现方案:独立贴图版 vs 图集适配版
  2. 图集适配原理 :UV重映射与_MainTex_ST参数
  3. 性能优化:MaterialPropertyBlock最佳实践
  4. Shader优化:减少计算、使用内置函数
  5. 生产应用:对象池、进度动画、完整组件

推荐后续学习:

  • 实现多色渐变进度条
  • 添加圆形/径向进度条支持
  • 实现进度条动画过渡效果
  • 优化大量实例的性能(GPU Instancing)
相关推荐
猫屋小鱼丸4 小时前
手把手教你在unity中实现一个视觉小说系统(一)
unity
国服第二切图仔7 小时前
Rust开发实战之简单游戏开发(piston游戏引擎)
开发语言·rust·游戏引擎
HahaGiver66617 小时前
Unity与Android原生交互开发入门篇 - 打开Unity游戏的设置
android·unity·交互
@LYZY18 小时前
Unity TextMeshPro 文本对齐方式详解
unity·游戏引擎·textmeshpro·tmp
在路上看风景19 小时前
2.1 ShaderLab - 渲染状态
unity
AA陈超21 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P07-06 能力输入的回调
c++·游戏·ue5·游戏引擎·虚幻
一线灵1 天前
跨平台游戏引擎 Axmol-2.9.1 发布
游戏引擎
地狱为王1 天前
Unity使用RVM实现实时人物视频抠像(无绿幕)
unity·游戏引擎·音视频
HahaGiver6661 天前
Unity与Android原生交互开发入门篇 - 打开Android的设置
android·java·unity·游戏引擎·android studio