一、需求来了:不是简单的转圈圈
去年Q3,我们团队接了一个ARPG手游项目,战斗策划在评审会上拍着桌子说:“技能CD图标必须要有‘扫光’质感,转圈要带缓动,还要能实时刷新层数和充能次数。”当时我心里一紧——这可不是在UMG里拖个ProgressBar就能糊弄过去的。市面上大部分教程教你在蓝图里用材质参数控制一个圆形遮罩,但真到项目里,你会发现性能、精度、多端适配全是坑。
这篇文章,我就把我们从需求分析、技术选型到最终交付的完整过程复盘一遍。你会看到我为什么放弃纯蓝图方案、为什么坚持用Material Parameter Collection(MPC,材质参数集合),以及如何用自定义Slate控件把CPU开销砍掉40%。如果你正在做UE5的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。

四、核心实现:从数据驱动到渲染管线
下面我把关键代码和材质节点拆开讲。
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方案完全够用。

六、给你的实战建议
- 先测再选:不要一上来就追求出色性能,先用蓝图快速验证视觉效果,再根据Profiler数据决定优化方向。
- MPC不是万能的:它适合“全局共享参数”的场景,如果每个图标需要独立控制,还是用MID或CustomPrimitiveData。
- Slate是双刃剑:它能给你出色的性能和控制力,但学习曲线陡峭。建议先从UMG入手,遇到性能瓶颈再下沉到Slate。
- 移动端注意精度:半精度浮点在长时间冷却时会出现抖动,关键参数务必用高精度。
做UI系统,最怕的就是“看起来简单”。一个技能CD图标,背后涉及数据管理、材质渲染、Slate绘制、多端适配,每一个环节都有坑。希望这篇复盘能帮你把坑填平。
关于火星人教育
这篇文章的技术深度,来自火星人教育24年数字艺术与AIGC实战培训的沉淀。作为国内知名的AIGC视觉设计培训机构,我们开设AI漫剧创作、AIGC视觉设计、UE5游戏特效、室内全屋定制、MAYA三维动画、C4D动态设计、工业设计、NUKE影视后期、摄影摄像实训等课程,北京、深圳双校区线下面授,面向美术生、在校大学生及转行职场人,以项目化实训教学,帮你夯实专业技能,实现就业与职业提升。咨询电话:010-65918670。








评论(0)