一、需求来了:不是简单的转圈圈

去年Q3,我们团队接了一个ARPG手游项目,战斗策划在评审会上拍着桌子说:“技能CD图标必须要有‘扫光’质感,转圈要带缓动,还要能实时刷新层数和充能次数。”当时我心里一紧——这可不是在UMG里拖个ProgressBar就能糊弄过去的。市面上大部分教程教你在蓝图里用材质参数控制一个圆形遮罩,但真到项目里,你会发现性能、精度、多端适配全是坑。

这篇文章,我就把我们从需求分析、技术选型到最终交付的完整过程复盘一遍。你会看到我为什么放弃纯蓝图方案、为什么坚持用Material Parameter Collection(MPC,材质参数集合),以及如何用自定义Slate控件把CPU开销砍掉40%。如果你正在做UE5的UI系统,这篇能帮你少走两周弯路。

UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘 - 配图1
UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘

二、技术选型:蓝图、材质、还是Slate?

先说说我们的项目背景:MOBA+ARPG混合玩法,同屏最多显示12个技能图标,每个图标需要展示冷却倒计时、充能层数、沉默/禁用状态。目标平台是中端安卓机(骁龙7系)和iOS A12以上。

2.1 方案A:纯蓝图+ProgressBar

最直觉的做法是UMG里放一个ProgressBar,绑定技能冷却剩余百分比。但问题很明显:ProgressBar的填充是线性的,做不出“扇形扫光”的效果;而且每帧更新12个进度条,每个都要走一遍蓝图Tick,在移动端就是灾难。我们实测过,12个ProgressBar同时刷新,GameThread的UI耗时直接飙到3.2ms,加上渲染线程的Overdraw,中端机直接掉到45帧。

2.2 方案B:材质遮罩+动态材质实例

这是大多数教程推荐的路子:用一张圆形遮罩贴图,通过材质里的Rotator节点旋转UV,再用一个标量参数控制旋转角度。蓝图里每帧把冷却进度Set Scalar Parameter Value到动态材质实例。这个方案视觉上没问题,但性能依然堪忧——每个图标要创建一个MID(Material Instance Dynamic,动态材质实例),12个MID意味着12次Draw Call,而且每次Set参数都会触发材质重新编译检查,移动端上开销不小。

2.3 方案C:MPC+自定义Slate

最终我们选了这条路。核心思路是:所有技能图标的冷却参数写入一个MPC,材质里通过CollectionParameter节点读取,这样全局共享一个材质,Draw Call合并成1次。同时,图标的布局和交互逻辑用自定义Slate控件实现,把每帧的Tick逻辑从蓝图搬到C++,减少反射调用开销。

技巧提示:MPC的标量参数上限是1024个,对于技能CD场景完全够用。但要注意,MPC更新是全局的,适合“同屏所有图标共享同一套参数”的情况。如果你的图标需要独立控制,还是得用MID。

三、踩坑实录:那些教程不会告诉你的细节

方案定了,动手吧。结果第一个版本就翻车了。

3.1 坑一:材质里的时间精度问题

我们最初用Time节点驱动扫光旋转,蓝图里传一个“剩余时间”参数。但移动端上Time节点是半精度浮点,当冷却时间大于60秒时,角度计算出现明显抖动。后来改用自定义时间参数,由C++每帧传入高精度时间戳,抖动才消失。

3.2 坑二:UI材质与粒子系统的排序冲突

技能冷却扫光需要半透明叠加,但我们的角色身上有粒子特效也是半透明的。在移动端,UI和粒子的渲染顺序由Translucency Sort Priority控制。我们忘了设置这个值,结果冷却图标被角色特效遮挡。解决办法是在Slate控件里强制设置ZOrder,并在材质里把Blend Mode设为Translucent,同时勾选Disable Depth Test。

3.3 坑三:动态合批失效

本以为用了MPC就能自动合批,结果Profiler里发现还是12个Draw Call。排查后发现,每个图标的材质实例虽然共享父材质,但UMG里每个Image控件会生成独立的Slate元素,它们不参与动态合批。最终我们重写了Slate控件,把所有图标绘制在一个自定义DrawCall里,用FSlateDrawElement::MakeCustomVerts手动提交顶点,才把Draw Call压到1。

UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘 - 配图2
UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘

四、核心实现:从数据驱动到渲染管线

下面我把关键代码和材质节点拆开讲。

4.1 数据结构设计

每个技能图标对应一个FSkillCooldownData结构体:

struct FSkillCooldownData
{
    float RemainingTime;      // 剩余冷却时间
    float TotalTime;          // 总冷却时间
    int32 ChargeCount;        // 当前充能层数
    int32 MaxChargeCount;     // 最大充能层数
    bool bIsSilenced;         // 是否被沉默
    FVector2D IconPosition;   // 图标在屏幕上的位置
};

这些数据由USkillCooldownSubsystem(一个GameInstanceSubsystem)统一管理,每帧更新后写入MPC。

4.2 MPC参数映射

我们在MPC里定义了一个Scalar数组,长度为12*4=48个参数。每个图标占4个:剩余时间、总时间、充能层数、禁用状态。材质里通过CollectionParameter节点读取,索引由图标的CustomPrimitiveData传入。这里有个关键点:CustomPrimitiveData是UE5新增的机制,允许每个Primitive拥有独立的数据,而不需要创建MID。我们把它用在UI的Slate元素上,实现了“共享材质+独立参数”的效果。

4.3 材质节点逻辑

核心节点链路:CollectionParameter(RemainingTime)除以CollectionParameter(TotalTime)得到冷却进度,然后通过Rotator节点把UV旋转进度*360度,再用RadialGradientExponential生成扇形遮罩。最后用Lerp混合正常图标和灰度图标,实现禁用状态。

注意:Rotator节点的中心点默认是(0.5,0.5),如果你的图标不是正方形,需要手动调整中心点,否则扫光会偏心。

4.4 Slate控件绘制

我们继承SCompoundWidget,在OnPaint函数里遍历所有技能数据,用FSlateDrawElement::MakeBox绘制图标底图,再用MakeCustomVerts绘制扫光扇形。这样所有图标共享一个Draw Call,CPU开销从每帧3.2ms降到0.8ms。

方案 Draw Call CPU耗时(12图标) 移动端帧率
纯蓝图+ProgressBar 12 3.2ms 45 FPS
MID+材质遮罩 12 2.1ms 52 FPS
MPC+自定义Slate 1 0.8ms 60 FPS

五、交付与复盘:数据不会说谎

最终版本上线后,我们做了完整的性能对比。在中端安卓机上,12个技能图标同屏时,UI线程耗时稳定在0.8ms以内,Draw Call为1,帧率稳定60FPS。对比最初的蓝图方案,性能提升了4倍。

但我也要客观说,这个方案不是银弹。它的代价是代码复杂度高,需要C++和Slate基础,而且调试不如蓝图直观。如果你的项目是PC端、同屏图标少于5个,用MID方案完全够用。

UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘 - 配图3
UE5 技能CD冷却图标UI:从踩坑到落地的项目实战复盘

六、给你的实战建议

  • 先测再选:不要一上来就追求出色性能,先用蓝图快速验证视觉效果,再根据Profiler数据决定优化方向。
  • MPC不是万能的:它适合“全局共享参数”的场景,如果每个图标需要独立控制,还是用MID或CustomPrimitiveData。
  • Slate是双刃剑:它能给你出色的性能和控制力,但学习曲线陡峭。建议先从UMG入手,遇到性能瓶颈再下沉到Slate。
  • 移动端注意精度:半精度浮点在长时间冷却时会出现抖动,关键参数务必用高精度。

做UI系统,最怕的就是“看起来简单”。一个技能CD图标,背后涉及数据管理、材质渲染、Slate绘制、多端适配,每一个环节都有坑。希望这篇复盘能帮你把坑填平。

关于火星人教育

这篇文章的技术深度,来自火星人教育24年数字艺术与AIGC实战培训的沉淀。作为国内知名的AIGC视觉设计培训机构,我们开设AI漫剧创作、AIGC视觉设计、UE5游戏特效、室内全屋定制、MAYA三维动画、C4D动态设计、工业设计、NUKE影视后期、摄影摄像实训等课程,北京、深圳双校区线下面授,面向美术生、在校大学生及转行职场人,以项目化实训教学,帮你夯实专业技能,实现就业与职业提升。咨询电话:010-65918670。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。