当前位置: 首页 > news >正文

UGUI源码剖析(12):实战演练——从零构建一个健壮的Gradient顶点特效

UGUI源码剖析(第十二章):实战演练——从零构建一个健壮的Gradient顶点特效

在前一章中,我们深入剖析了BaseMeshEffect的底层机制。现在,是时候将这些理论知识付诸实践了。本章,我们将扮演一名“UI特效工程师”,从一个空白的C#脚本文件开始,一步步地、从零到一地,构建出一个功能强大、健壮且高效的Gradient(渐变)特效组件。

第一步:基础框架与“垂直渐变”的初次尝试 (Version 1.0)

我们的第一个目标,是实现一个最基础的、只能从下到上进行垂直渐变的功能。

1.1 创建基础脚本

首先,我们创建一个名为GradientEffect.cs的脚本,并让它继承自BaseMeshEffect。

// GradientEffect.cs (V1.0)
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;[AddComponentMenu("UI/Effects/Extensions/Gradient Effect")]
public class GradientEffect : BaseMeshEffect
{public Color colorTop = Color.white;public Color colorBottom = Color.black;// 核心的ModifyMesh方法,暂时留空public override void ModifyMesh(VertexHelper vh){if (!IsActive()){return;}// TODO: 在这里实现我们的渐变逻辑}
}
  • 所有顶点特效都必须继承自BaseMeshEffect。
  • 核心逻辑必须在覆写的ModifyMesh(VertexHelper vh)方法中实现。
  • IsActive()检查是标准的安全实践,确保只在组件激活时才执行逻辑。

1.2 实现垂直渐变算法

现在,我们来填充ModifyMesh的逻辑。我们的目标是,让UI元素的底部顶点应用colorBottom,顶部顶点应用colorTop,中间的顶点则根据其Y轴位置进行线性插值。

 // GradientEffect.cs (V1.0) -> ModifyMesh
public override void ModifyMesh(VertexHelper vh)
{if (!IsActive()) return;// 1. 获取顶点列表List<UIVertex> vertexList = new List<UIVertex>();vh.GetUIVertexStream(vertexList);int count = vertexList.Count;if (count == 0) return;// 2. 找到整个几何体的Y轴边界float bottomY = vertexList[0].position.y;float topY = vertexList[0].position.y;for (int i = 1; i < count; i++){float y = vertexList[i].position.y;if (y > topY) topY = y;else if (y < bottomY) bottomY = y;}// 3. 遍历并修改每一个顶点float uiElementHeight = topY - bottomY;UIVertex vertex = new UIVertex();for (int i = 0; i < count; i++){vertex = vertexList[i];// 计算当前顶点的归一化高度 (0-1)float normalizedY = (vertex.position.y - bottomY) / uiElementHeight;// 根据归一化高度,在起始色和结束色之间进行线性插值Color finalColor = Color.Lerp(colorBottom, colorTop, normalizedY);// 将渐变色与顶点的原始颜色相乘,以保留富文本等效果vertex.color = vertex.color * finalColor;vertexList[i] = vertex;}// 4. 将修改后的顶点列表写回vh.Clear();vh.AddUIVertexTriangleStream(vertexList);
}
  • AABB计算:通过一次完整的遍历,计算出了几何体的**轴对齐包围盒(AABB)**的Y轴边界。这是保证算法健壮性的关键。
  • 归一化与插值:(当前值 - 最小值)/ 范围是计算归一化位置的标准数学公式。Color.Lerp()则是实现平滑过渡的核心函数。
  • 颜色混合:我们使用*= (乘法)来混合颜色,而不是直接赋值=。这是一种良好的实践,它能保留Graphic自身(如Image.color)以及富文本标签所赋予的原始颜色和透明度。

至此,我们的V1.0版本已经完成。它已经是一个功能正确且健壮的垂直渐变特效了。

在这里插入图片描述

第二步:功能扩展——支持多种渐变方向 (Version 2.0)

现在,我们需要增加水平渐变和四角渐变的功能。

2.1 增加枚举和公共属性

// GradientEffect.cs (V2.0)
public class GradientEffect : BaseMeshEffect
{public enum GradientDirection { Vertical, Horizontal, FourCorners }public GradientDirection direction = GradientDirection.Vertical;// 将颜色属性扩展为四个角public Color colorTop = Color.white;public Color colorBottom = Color.black;public Color colorLeft = Color.white;public Color colorRight = Color.black;// ...
}

2.2 扩展ModifyMesh的算法

我们需要在循环中,根据不同的direction来执行不同的颜色计算逻辑。

// GradientEffect.cs (V2.0) -> ModifyMesh// 步骤 1: 精确计算UI元素的几何边界 (轴对齐包围盒, AABB)。// 这一步对于让渐变效果在复杂Sprite上(如Sliced, Tiled或有紧密网格的Sprite)正确工作至关重要。float bottomY = vertexList[0].position.y;float topY = vertexList[0].position.y;float leftX = vertexList[0].position.x;float rightX = vertexList[0].position.x;for (int i = 1; i < count; i++){float y = vertexList[i].position.y;if (y > topY){topY = y;}else if (y < bottomY){bottomY = y;}float x = vertexList[i].position.x;if (x > rightX){rightX = x;}else if (x < leftX){leftX = x;}}float uiElementHeight = topY - bottomY;float uiElementWidth = rightX - leftX;// 步骤 2: 遍历每一个顶点,并应用计算出的渐变颜色。UIVertex vertex = new UIVertex();for (int i = 0; i < count; i++){vertex = vertexList[i];Color colorMultiplier;switch (direction){case GradientDirection.Vertical:// 计算归一化的垂直位置(0在底部,1在顶部)。float normalizedY = (uiElementHeight > 0) ? (vertex.position.y - bottomY) / uiElementHeight : 0f;colorMultiplier = Color.Lerp(colorBottom, colorTop, normalizedY);break;case GradientDirection.Horizontal:// 计算归一化的水平位置(0在左侧,1在右侧)。float normalizedX = (uiElementWidth > 0) ? (vertex.position.x - leftX) / uiElementWidth : 0f;colorMultiplier = Color.Lerp(colorLeft, colorRight, normalizedX);break;case GradientDirection.FourCorners:// 计算水平和垂直的归一化位置。float hLerp = (uiElementWidth > 0) ? (vertex.position.x - leftX) / uiElementWidth : 0f;float vLerp = (uiElementHeight > 0) ? (vertex.position.y - bottomY) / uiElementHeight : 0f;// 进行双线性插值。// 首先,沿着底部和顶部边缘,根据水平位置进行线性插值。Color bottomLerp = Color.Lerp(colorBottom, colorLeft, hLerp); // 假设 colorBottom是左下, colorLeft是右下Color topLerp = Color.Lerp(colorTop, colorRight, hLerp);      // 假设 colorTop是左上, colorRight是右上// 然后,根据垂直位置,在上下两条边的插值结果之间,再次进行线性插值。colorMultiplier = Color.Lerp(bottomLerp, topLerp, vLerp);break;default:colorMultiplier = Color.white;break;}// 将计算出的渐变色 与 顶点的原始颜色 进行正片叠底(相乘)。// 这样做可以保留原有的颜色信息(例如来自富文本标签的颜色)以及Graphic组件的主颜色。// UIVertex.color是Color32类型,所以我们先转成Color进行运算,再转回去。var originalColor = (Color)vertex.color;vertex.color = (Color32)(originalColor * colorMultiplier);vertexList[i] = vertex;}
  • 双线性插值 (Bilinear Interpolation):四角渐变的本质是双线性插值。我们首先根据顶点的水平位置,分别在顶部边缘底部边缘进行一次线性插值,得到两个中间色。然后,再根据顶点的垂直位置,在这两个中间色之间,进行第二次线性插值,从而得到最终的颜色。这个算法确保了颜色在二维平面上的平滑过渡。
  • 代码的扩展性:通过if-else或switch结构,我们可以轻松地为组件增加更多、更复杂的渐变模式,而无需修改其基础框架。

在这里插入图片描述平铺模式保证渐变正确性

**第三步:性能优化——告别GC **

我们的V2.0版本功能已经完备,但存在一个性能隐患:List vertexList = new List()。在ModifyMesh这个可能被高频调用的方法中,new一个列表,会产生不必要的GC垃圾(GC Alloc)。现在,我们来修复它。

// GradientEffect.cs 
using UnityEngine.Pool; // 引入对象池命名空间public class GradientEffect : BaseMeshEffect
{// ... (属性不变) ...public override void ModifyMesh(VertexHelper vh){if (!IsActive() || vh.currentVertCount == 0) return;// 1. 从对象池获取一个列表实例var vertexList = ListPool<UIVertex>.Get();vh.GetUIVertexStream(vertexList);// 2. 将核心逻辑包裹在try-finally中try {// 3. 调用我们已经写好的核心修改逻辑ModifyVertices(vertexList);// 4. 将修改后的顶点流写回vh.Clear();vh.AddUIVertexTriangleStream(vertexList);}finally{// 5. 确保无论如何,列表都会被归还到池中ListPool<UIVertex>.Release(vertexList);}}// 将核心逻辑封装到一个私有方法中,提高代码清晰度private void ModifyVertices(List<UIVertex> vertexList){// ... (这里是V2.0版本中完整的AABB计算和顶点颜色修改循环) ...}
}
  • ListPool: 这是Unity提供的一个高效的内置列表对象池。Get()方法会从池中取出一个可复用的列表实例(如果池为空则new一个),而Release()则会将其归还到池中。
  • try-finally结构:这是一个极其重要的健壮性保证。将Release()调用放在finally块中,可以确保即使ModifyVertices或AddUIVertexTriangleStream等方法因为某些原因抛出异常,vertexList这个从池中“借”出的资源,也一定会被归还。这有效地防止了对象池的内存泄漏。

总结:

通过这次从零到一的实战演练,我们不仅构建了一个功能完备、健壮且高性能的Gradient特效,更重要的是,我们掌握了一套开发自定义UI特效的标准工程流程

  1. 从最简功能开始:先实现核心算法,确保逻辑正确。
  2. 逐步扩展功能:在基础框架上,通过增加属性和算法分支,来丰富组件的功能。
  3. 最后审视性能:在功能稳定后,检查代码中的性能热点(如GC Alloc),并使用对象池等标准技术进行优化。

这个流程,以及我们在其中运用的AABB计算、线性/双线性插值、对象池等技术,不仅适用于Gradient特效。它们是构建任何一个高质量BaseMeshEffect组件的、可被复用的“思想武器库”。掌握了它们,你就拥有了将任何创意,转化为高效、稳定UI视觉效果的强大能力。

以下附上完整代码:

using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEngine.Pool;[AddComponentMenu("UI/Effects/Extensions/Gradient Effect")]
public class GradientEffect : BaseMeshEffect
{/// <summary>/// 渐变的方向。/// </summary>public enum GradientDirection{Vertical,       // 垂直Horizontal,     // 水平FourCorners     // 四角}[Tooltip("渐变的应用方向。")]public GradientDirection direction = GradientDirection.Vertical;[Header("垂直渐变")][Tooltip("UI元素顶部的颜色。")]public Color colorTop = new Color(1f, 1f, 1f, 1f);[Tooltip("UI元素底部的颜色。")]public Color colorBottom = new Color(0f, 0f, 0f, 1f);[Header("水平渐变")][Tooltip("UI元素左侧的颜色。")]public Color colorLeft = new Color(1f, 1f, 1f, 1f);[Tooltip("UI元素右侧的颜色。")]public Color colorRight = new Color(0f, 0f, 0f, 1f);// 备注: 对于四角渐变模式,以上四个颜色属性都会被使用。// colorBottom 代表左下角,colorLeft 代表右下角// colorTop 代表左上角,colorRight 代表右上角public override void ModifyMesh(VertexHelper vh){// 如果组件未激活,或没有顶点,则不执行任何操作。if (!IsActive() || vh.currentVertCount == 0){return;}// 使用ListPool从对象池中获取一个临时的顶点列表,以避免GC开销。var vertexList = ListPool<UIVertex>.Get();vh.GetUIVertexStream(vertexList);try{// 核心的顶点修改逻辑被封装在此方法中。ModifyVertices(vertexList);// 清空VertexHelper,并将修改后的顶点流重新写入。vh.Clear();vh.AddUIVertexTriangleStream(vertexList);}finally{// 使用try-finally结构,确保即使发生异常,列表也一定会被释放回对象池。ListPool<UIVertex>.Release(vertexList);}}private void ModifyVertices(List<UIVertex> vertexList){int count = vertexList.Count;if (count == 0) return;// 步骤 1: 精确计算UI元素的几何边界 (轴对齐包围盒, AABB)。// 这一步对于让渐变效果在复杂Sprite上(如Sliced, Tiled或有紧密网格的Sprite)正确工作至关重要。float bottomY = vertexList[0].position.y;float topY = vertexList[0].position.y;float leftX = vertexList[0].position.x;float rightX = vertexList[0].position.x;for (int i = 1; i < count; i++){float y = vertexList[i].position.y;if (y > topY){topY = y;}else if (y < bottomY){bottomY = y;}float x = vertexList[i].position.x;if (x > rightX){rightX = x;}else if (x < leftX){leftX = x;}}float uiElementHeight = topY - bottomY;float uiElementWidth = rightX - leftX;// 步骤 2: 遍历每一个顶点,并应用计算出的渐变颜色。UIVertex vertex = new UIVertex();for (int i = 0; i < count; i++){vertex = vertexList[i];Color colorMultiplier;switch (direction){case GradientDirection.Vertical:// 计算归一化的垂直位置(0在底部,1在顶部)。float normalizedY = (uiElementHeight > 0) ? (vertex.position.y - bottomY) / uiElementHeight : 0f;colorMultiplier = Color.Lerp(colorBottom, colorTop, normalizedY);break;case GradientDirection.Horizontal:// 计算归一化的水平位置(0在左侧,1在右侧)。float normalizedX = (uiElementWidth > 0) ? (vertex.position.x - leftX) / uiElementWidth : 0f;colorMultiplier = Color.Lerp(colorLeft, colorRight, normalizedX);break;case GradientDirection.FourCorners:// 计算水平和垂直的归一化位置。float hLerp = (uiElementWidth > 0) ? (vertex.position.x - leftX) / uiElementWidth : 0f;float vLerp = (uiElementHeight > 0) ? (vertex.position.y - bottomY) / uiElementHeight : 0f;// 进行双线性插值。// 首先,沿着底部和顶部边缘,根据水平位置进行线性插值。Color bottomLerp = Color.Lerp(colorBottom, colorLeft, hLerp); // 假设 colorBottom是左下, colorLeft是右下Color topLerp = Color.Lerp(colorTop, colorRight, hLerp);      // 假设 colorTop是左上, colorRight是右上// 然后,根据垂直位置,在上下两条边的插值结果之间,再次进行线性插值。colorMultiplier = Color.Lerp(bottomLerp, topLerp, vLerp);break;default:colorMultiplier = Color.white;break;}// 将计算出的渐变色 与 顶点的原始颜色 进行正片叠底(相乘)。// 这样做可以保留原有的颜色信息(例如来自富文本标签的颜色)以及Graphic组件的主颜色。// UIVertex.color是Color32类型,所以我们先转成Color进行运算,再转回去。var originalColor = (Color)vertex.color;vertex.color = (Color32)(originalColor * colorMultiplier);vertexList[i] = vertex;}}
}
http://www.dtcms.com/a/342600.html

相关文章:

  • 虚幻基础:目标值之间的过渡
  • 数字货币发展存在的问题:交易平台的问题不断,但监管日益加强
  • C++ string类(c_str , find和rfind , npos , find_first_of)
  • DeepSeek V3.1正式发布,专为下代国产芯设计
  • 【LeetCode 热题 100】322. 零钱兑换——(解法二)自底向上
  • 2025年物流大数据分析的主要趋势
  • 血缘元数据采集开放标准:OpenLineage Dataset Facets
  • Python-Pandas GroupBy 进阶与透视表学习
  • 如何用算力魔方4060安装PaddleOCR MCP 服务器
  • 实现自己的AI视频监控系统-第一章-视频拉流与解码3
  • JavaWeb前端03(Ajax概念及在前端开发时应用)
  • Windows下,将本地视频转化成rtsp推流的方法
  • 高效处理NetCDF文件经纬度转换:一个纯CDO驱动的Bash脚本详解
  • GitHub 热榜项目 - 日榜(2025-08-21)
  • 009.Redis Predixy + Sentinel 架构
  • 深度卷积神经网络AlexNet
  • 【NVIDIA-B200】生产报错 Test CUDA failure common.cu:1035 ‘system not yet initialized‘
  • Docker 搭建 Gitlab 实现自动部署Vue项目
  • NW755NW776美光固态闪存NW863NX595
  • 【永洪BI】报告脚本-JavaScript使用【完整版】
  • Vue 项目中父子传值使用Vuex异步数据不更新问题
  • Postman来做API安全测试:身份验证缺陷漏洞测试
  • 药品追溯码(溯源码)采集系统(二):门诊发药后端
  • 【Linux系统】进程信号:信号的产生和保存
  • 使用EasyExcel 导出复杂的合并单元格
  • 第四届中国高校机器人实验教学创新大赛团队参赛总结
  • selenium一些进阶方法如何使用
  • 大模型0基础开发入门与实践:第11章 进阶:LangChain与外部工具调用
  • 打破传统课程模式,IP变现的创新玩法 | 创客匠人
  • 从零开始学 Selenium:浏览器驱动、元素定位与实战技巧