多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity UI动效提效脚本:自动保存、图片压缩、图集估算与动画末帧对齐

Unity UI动效提效脚本:自动保存、图片压缩、图集估算与动画末帧对齐 1. 为什么我要折腾这6个UI动效提效脚本做Unity UI动效这行的朋友应该都有体会项目一旦进入中后期动效师和UI程序之间那种反复拉扯的消耗感特别明显。美术在Animation窗口里一帧一帧K完的动画导入引擎后末帧对不齐、偏移量飘了、图集爆了、工程崩了没保存——这些破事几乎每个项目都会遇到。我过去两年在三个中型手游项目里负责UI动效管线踩过的坑足够写一本小册子后来索性把高频重复的操作全部脚本化攒下来6个自己每天都在用的工具覆盖自动保存、图片压缩、图集估算、动画末帧对齐与偏移修正这几个最痛的环节。这6个脚本全部基于Unity Editor原生API开发不依赖任何第三方插件拷贝到工程里就能跑。它们解决的问题非常具体自动保存脚本让你在编辑器崩溃时少丢半小时工作量图片压缩脚本把UI切图批量处理成符合项目规范的格式省掉逐张手动改Import Settings的时间图集估算脚本在打包前告诉你当前图集会不会超尺寸、超了大概多少动画末帧与偏移脚本则专门处理动效播放完回弹、位置对不上的经典问题。适合所有做Unity UI动效的从业者不管你是刚入行的新人还是带团队的主程只要你的日常和Animation、Sprite、Atlas打交道这些脚本都能直接抄作业。我写这些脚本的原则就一条不搞花架子每个脚本解决一个具体问题代码尽量短注释尽量清楚参数尽量暴露在Inspector上让美术也能自己调。下面我按模块拆开讲每个脚本都会说清楚它为什么这么设计、核心API怎么用、参数怎么算、以及我在实际项目里踩过的坑。2. 脚本整体设计与选型思路拆解2.1 为什么全部用Editor脚本而不是Runtime脚本这6个工具全部放在Editor文件夹下继承EditorWindow或使用InitializeOnLoad特性。原因很直接它们操作的是资源导入、动画剪辑、图集配置这些编辑器层面的东西运行时根本用不到。如果做成Runtime脚本不仅会增加包体还会因为Unity的序列化机制导致一些莫名其妙的引用丢失问题。另一个考虑是权限和安全性。Editor脚本可以访问AssetDatabase、EditorUtility、Selection这些编辑器专属API批量修改资源导入设置时效率比Runtime高一个数量级。比如图片压缩脚本要遍历几百张Texture用AssetDatabase.StartAssetEditing()和StopAssetEditing()包起来能把导入刷新从几分钟压到几秒。注意Editor脚本里千万不要用Resources.Load或者直接File.ReadAllBytes去读工程内资源一定要走AssetDatabase.LoadAssetAtPath否则拿到的可能是未导入的原始文件路径在不同机器上还会出问题。2.2 脚本之间的协作关系这6个脚本不是孤立的它们在实际工作流里是有先后顺序的。我通常的流程是美术出图后先用图片压缩脚本批量处理Import Settings然后用图集估算脚本检查当前图集规划是否合理动效K完后用动画末帧脚本做批量校验和修正自动保存脚本则全程在后台跑着兜底。这种设计的好处是每个脚本职责单一出问题容易定位。比如图集估算不准那肯定是TextureImporter的maxTextureSize或者压缩格式读错了不会牵扯到动画逻辑。我见过有些团队把十几个功能塞进一个“超级工具”里结果改一个参数崩三个功能维护成本极高。2.3 参数暴露策略让美术也能自己调所有脚本的关键参数我都尽量暴露在Inspector或者EditorWindow的GUI上而不是写死在代码里。比如图片压缩脚本的压缩格式、最大尺寸、是否生成Mipmap图集估算脚本的图集最大尺寸、Padding、是否允许旋转动画末帧脚本的容差阈值、是否自动修正偏移。这么做是因为实际项目里不同品类的UI规范差异很大。休闲游戏可能UI切图最大512就够了重度MMO可能要到2048。如果参数写死每个项目都要改代码很容易漏改导致线上事故。暴露出来之后美术组长自己就能根据项目规范调程序只需要保证逻辑正确。3. 自动保存脚本编辑器崩溃时的最后一道防线3.1 核心原理与触发时机Unity编辑器本身有自动保存场景的机制但那个只保存Scene不保存Prefab、AnimationClip、ScriptableObject这些资源。而且它的触发条件是“有未保存修改且空闲一段时间”实际用下来经常在你最需要的时候不触发。我自己写的自动保存脚本核心逻辑很简单定时检查EditorApplication.isPlaying和EditorApplication.isCompiling如果都不在运行且当前有未保存修改就调用AssetDatabase.SaveAssets()。触发时机我设了三个一是固定时间间隔默认5分钟二是编辑器失去焦点时比如你切到浏览器查资料三是进入Play模式前强制保存一次。第三个特别重要因为很多崩溃发生在Play模式切换的瞬间尤其是场景里有大量粒子或Shader编译的时候。[InitializeOnLoad] public static class AutoSave { static AutoSave() { EditorApplication.update OnUpdate; EditorApplication.playModeStateChanged OnPlayModeChanged; } static double lastSaveTime; const double SaveInterval 300.0; // 5分钟 static void OnUpdate() { if (EditorApplication.isPlaying || EditorApplication.isCompiling) return; if (EditorApplication.timeSinceStartup - lastSaveTime SaveInterval) return; if (!EditorApplication.isDirty) return; AssetDatabase.SaveAssets(); lastSaveTime EditorApplication.timeSinceStartup; Debug.Log([AutoSave] 资源已自动保存); } static void OnPlayModeChanged(PlayModeStateChange state) { if (state PlayModeStateChange.ExitingEditMode) { AssetDatabase.SaveAssets(); } } }3.2 为什么不用EditorApplication.isDirty判断上面代码里用了EditorApplication.isDirty这个属性在Unity 2019之后才有老版本需要用EditorApplication.ModifyStatus或者自己维护一个脏标记。我实测下来isDirty在大多数情况下是准的但有一个坑如果你在脚本里用EditorUtility.SetDirty标记了某个资源但还没保存isDirty会返回true这时候自动保存会把这个中间状态也存进去。所以我在实际项目里会加一个白名单只对Scene和Prefab做自动保存ScriptableObject和AnimationClip还是让美术手动保存避免存到一半的中间数据。实操心得自动保存间隔不要设太短我试过1分钟一次结果每次保存都触发AssetDatabase刷新编辑器卡顿明显。5分钟是比较舒服的平衡点配合失去焦点保存基本不会丢超过2分钟的工作量。3.3 崩溃恢复的补充方案自动保存只能减少损失不能完全避免。我还会在工程里放一个EditorPrefs记录上次保存时间如果检测到上次退出是非正常退出比如通过EditorApplication.quitting标记下次启动时弹窗提示“上次可能异常退出是否打开备份场景”。这个逻辑不复杂但关键时刻能救命。备份场景我存在工程外的临时目录避免污染Assets文件夹。4. 图片压缩脚本批量处理UI切图的Import Settings4.1 为什么UI切图必须批量压缩UI切图的数量在项目中后期会爆炸式增长。一个中型项目光主界面加各种弹窗、图标、特效贴图轻松超过500张。如果每张都手动在Inspector里改Texture Type、Sprite Mode、Max Size、Compression一个人一天都搞不完而且极易漏改。漏改的后果很直接包体超标、内存暴涨、加载变慢。我写的图片压缩脚本核心就是遍历选中文件夹下的所有Texture根据文件名规则或所在目录自动匹配预设然后批量设置TextureImporter参数。比如所有放在UI/Atlas/目录下的图统一设成Sprite、Max Size 1024、Compression Normal Quality、不生成Mipmap放在UI/Background/下的设成Max Size 2048、Compression High Quality。4.2 关键参数的计算与选择Max Size怎么定我的经验公式是先看这张图在UI里的最大显示尺寸然后乘以Canvas Scaler的参考分辨率比例再向上取到最近的2的幂。比如一张背景图在1920x1080下占满全屏Canvas Scaler参考分辨率是1920x1080那Max Size至少2048。如果这张图只在手机竖屏用实际显示宽度是1080那1024其实也够但考虑到平板和折叠屏我还是会留到2048。Compression格式的选择更讲究。UI切图如果有大面积渐变或半透明用ASTC 6x6或ETC2 RGBA8如果是纯色图标用ASTC 8x8甚至10x10都看不出区别。我一般会写一个简单的启发式判断读取Texture的alpha通道覆盖率如果alpha几乎全不透明用RGB压缩如果alpha有渐变用RGBA。这个判断用Texture2D.GetPixels就能做虽然慢一点但批量处理时跑一次就够。public static void CompressTextures(string folder, int maxSize, TextureImporterCompression quality) { var guids AssetDatabase.FindAssets(t:Texture2D, new[] { folder }); try { AssetDatabase.StartAssetEditing(); foreach (var guid in guids) { var path AssetDatabase.GUIDToAssetPath(guid); var importer AssetImporter.GetAtPath(path) as TextureImporter; if (importer null) continue; importer.textureType TextureImporterType.Sprite; importer.maxTextureSize maxSize; importer.textureCompression quality; importer.mipmapEnabled false; importer.SaveAndReimport(); } } finally { AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } }4.3 批量处理的性能优化上面代码里用了StartAssetEditing和StopAssetEditing这是批量修改资源导入设置的标准做法。不加这两个每改一张图Unity都会重新导入一次500张图能跑半小时。加上之后所有修改攒在一起最后统一刷新时间能压到一两分钟。还有一个坑是SaveAndReimport在StartAssetEditing块里调用会报错必须放在StopAssetEditing之后。我一开始没注意结果脚本跑一半就崩了。正确的做法是在循环里只改importer属性不调用SaveAndReimport等StopAssetEditing之后再统一AssetDatabase.Refresh()。注意如果工程开了Version Control批量修改导入设置会产生大量.meta文件变更提交前一定要确认这些变更都是预期的避免把临时测试的修改也提交上去。5. 图集估算脚本打包前预判图集是否超标5.1 图集估算的核心算法Unity的Sprite Atlas在打包时会把所有包含的Sprite按矩形装箱算法排布如果排不下就会报错或者自动缩小。图集估算脚本要做的就是提前模拟这个过程告诉美术当前图集会不会超。核心算法分三步一是收集图集下所有Sprite的原始尺寸和Padding二是用简单的矩形装箱算法我用的Shelf算法够用且快模拟排布三是计算总占用面积和最大图集尺寸的比值。Shelf算法的逻辑很朴素把所有矩形按高度降序排列然后一行一行往图集里放放不下就换行。这个算法不是最优的但胜在快且结果稳定。实际项目里图集利用率能到70%以上就说明规划合理低于60%就有优化空间。5.2 参数计算与阈值设定图集最大尺寸我一般设2048或4096取决于目标平台。移动端2048是安全线超过这个尺寸在低端机上可能直接加载失败。Padding默认2像素如果图集里有大量小图标Padding可以降到1但要注意压缩后边缘可能出现渗色。估算脚本的输出我设计成三档绿色表示利用率低于70%且总尺寸未超黄色表示利用率70%-90%或接近最大尺寸红色表示已超或利用率超过90%。红色必须处理黄色建议处理。这个阈值是我踩过坑之后定的之前有个项目图集利用率到了95%打包没报错但运行时在部分机型上出现图集错乱查了两天才定位到是图集太满导致。状态利用率总尺寸建议操作绿色70%未超无需处理黄色70%-90%接近上限考虑拆分或压缩红色90%已超必须拆分或降尺寸5.3 与Unity原生Sprite Atlas的配合Unity 2018之后自带的Sprite Atlas功能其实已经能处理大部分图集需求但它的报错信息很不友好经常只说“packing failed”不告诉你具体哪张图导致。我的估算脚本会输出详细的矩形列表按面积降序排列美术一眼就能看出是哪张大图把图集撑爆了。这个信息在优化时特别有用通常把最大的两三张图单独拆出去图集利用率就能从红色降到绿色。实操心得图集估算最好在每次提交前跑一次我把它挂在了CI的pre-commit钩子上如果检测到红色状态就直接阻止提交强制美术先处理。这个策略执行了半年线上再没出过图集相关的加载问题。6. 动画末帧与偏移脚本解决动效回弹和位置对不齐6.1 末帧对齐问题的根源UI动效里最常见的问题就是动画播放完UI元素的位置和动画开始前对不上。原因通常有三个一是AnimationClip的末帧关键帧值和初始值不一致二是动画用了Position偏移但没在末帧归零三是动画曲线用了Ease Out但末帧没到精确值。这三个问题在Animation窗口里肉眼很难发现因为差个几像素看不出来但播放多次或者和其他动画叠加时就会累积偏移。我的脚本核心逻辑是遍历选中的AnimationClip读取每一条Position和Scale曲线检查末帧值是否等于初始值或者等于指定的目标值如果差值超过容差阈值就标记出来。对于Position曲线还会额外检查X、Y、Z三个分量是否在末帧都归零。6.2 容差阈值的设定与计算容差阈值我默认设0.01这是基于Unity的浮点精度和UI像素密度算出来的。假设Canvas参考分辨率是1920x1080一个UI单位对应1像素0.01单位就是0.01像素肉眼绝对看不出来。但如果项目用了高DPI屏幕比如4K那0.01单位可能对应0.04像素还是看不出来。所以0.01是个安全的默认值。对于Scale曲线容差可以放宽到0.001因为Scale的基准值通常是10.001的相对误差是0.1%足够精确。如果动画里有从0缩放到1的效果末帧必须精确到1否则UI会看起来比设计稿小一圈。public static void CheckClipEndFrame(AnimationClip clip, float posTolerance 0.01f, float scaleTolerance 0.001f) { var bindings AnimationUtility.GetCurveBindings(clip); foreach (var binding in bindings) { var curve AnimationUtility.GetEditorCurve(clip, binding); if (curve null || curve.length 0) continue; var lastKey curve[curve.length - 1]; if (binding.propertyName.Contains(m_LocalPosition)) { if (Mathf.Abs(lastKey.value) posTolerance) { Debug.LogWarning($[AnimCheck] {clip.name} 的 {binding.path} 末帧Position偏移 {lastKey.value}); } } else if (binding.propertyName.Contains(m_LocalScale)) { if (Mathf.Abs(lastKey.value - 1f) scaleTolerance) { Debug.LogWarning($[AnimCheck] {clip.name} 的 {binding.path} 末帧Scale异常 {lastKey.value}); } } } }6.3 自动修正偏移的实现检测出来之后脚本还提供一键修正功能把所有Position曲线的末帧值强制设为0Scale曲线的末帧值强制设为1。这个操作要谨慎因为有些动画是故意让UI停在偏移位置的比如弹窗从屏幕外飞入后停在中间这种末帧Position本来就不该是0。所以我在脚本里加了一个“忽略路径”列表美术可以把这类动画的路径填进去脚本就跳过不处理。修正的实现用AnimationUtility.SetEditorCurve把末帧关键帧的值改掉再写回去。注意改完之后要调用EditorUtility.SetDirty和AssetDatabase.SaveAssets否则修改不会持久化。注意批量修正前一定要先备份或者用版本控制提交一次我吃过亏有一次脚本逻辑写错了把一批动画的末帧全改成了0结果弹窗动画全飞到屏幕左上角回滚花了不少时间。7. 常见问题与排查技巧实录7.1 脚本不生效的排查顺序脚本拷进工程后没反应按这个顺序查第一确认脚本放在Editor文件夹下不在Editor下的脚本访问不了AssetDatabase第二确认脚本没有编译错误Unity控制台如果有红色报错所有Editor脚本都不会执行第三确认菜单路径没冲突我习惯把菜单放在“Tools/UITools/”下避免和第三方插件撞车第四如果是InitializeOnLoad的脚本确认静态构造函数没有抛异常异常会导致整个回调链断掉。7.2 批量操作导致编辑器卡死的处理批量处理几百张图或动画时编辑器卡死是常事。我的经验是第一操作前先保存场景和资源卡死之后强制退出至少不丢数据第二用EditorUtility.DisplayProgressBar显示进度虽然会拖慢一点但至少知道卡在哪一步第三如果卡死超过5分钟直接任务管理器结束进程不要等Unity的批量操作一旦死循环基本救不回来。问题现象可能原因解决方法脚本菜单不显示编译错误或不在Editor文件夹查控制台确认路径批量处理卡死未用StartAssetEditing加上批量编辑包裹图集估算不准Padding或MaxSize读错检查Sprite Atlas配置动画末帧修正无效未SetDirty或未保存补上保存逻辑自动保存不触发isDirty判断失效改用脏标记白名单7.3 版本兼容性避坑这些脚本我在Unity 2019、2020、2021、2022上都跑过大部分API是稳定的但有几个点要注意EditorApplication.isDirty在2019之前没有需要条件编译TextureImporter.textureCompression在2020之后推荐用TextureImporterSettingsAnimationUtility.GetCurveBindings在2018之前叫GetAnimatableBindings。如果项目要兼容多个版本用#if UNITY_2019_1_OR_NEWER这类宏包起来。实操心得我习惯在每个脚本头部写清楚适用的Unity版本和最后测试日期团队里其他人拿到脚本一眼就知道能不能用。这个习惯看起来小但省了很多沟通成本。8. 我在实际项目里的一些体会这套脚本在我手上跑了两年多最大的感受是工具的价值不在于功能多而在于能不能让美术和程序少吵架。自动保存让美术不再因为崩溃丢工作而迁怒程序图片压缩让程序不再因为包体超标被运营追着跑图集估算让打包前的焦虑少了一大半动画末帧脚本则把动效验收从“肉眼看不出来就行”变成了有据可查的量化标准。如果非要挑一个最推荐的我会选动画末帧脚本。UI动效的偏移问题太隐蔽了一个动画偏2像素十个动画叠加就是20像素玩家不一定说得出来哪里不对但就是觉得“手感怪”。这个脚本把这种玄学问题变成了可检测、可修正的工程问题价值最大。最后分享一个小技巧这些脚本可以组合成一个EditorWindow用Tab分页管理美术只需要打开一个窗口就能完成所有操作。我现在的做法是把6个功能做成6个Tab每个Tab里放对应的参数和按钮窗口标题就叫“UI动效工具箱”。这样比散落在菜单里好用得多新来的美术培训十分钟就能上手。
返回列表