Unity MyFramework 用法说明(二十):使用 KeyFrameManager 统一管理缓动曲线

📅 2026/7/29 16:45:11
Unity MyFramework 用法说明(二十):使用 KeyFrameManager 统一管理缓动曲线
前一篇通过TimeManager实现了暂停和慢动作其中时间缩放的渐变过程就是由KeyFrameManager驱动的。在 MyFramework 中移动、旋转、缩放、透明度、摄像机参数和普通数值渐变都可以使用同一套关键帧曲线。项目地址https://github.com/ZHOURUIH/MyFramework一、KeyFrameManager 解决什么问题游戏中经常需要编写缓动动画界面弹出 按钮缩放 角色移动 图片渐隐 摄像机视野变化 数值平滑增长如果每个功能都自己计算进度mTime elapsedTime; float percent mTime / mDuration; float value Mathf.Lerp(mStart, mTarget, percent);不仅会产生大量重复代码各个系统使用的缓动方式也难以统一。MyFramework 将变化过程拆成两部分KeyFrameManager 负责提供曲线值 ComponentKeyFrame 负责播放曲线并驱动具体属性业务代码只需要指定使用什么曲线 从什么值变化到什么值 变化需要多长时间二、使用内置曲线框架已经提供了多种常用曲线ZERO_ONE 从 0 线性变化到 1 ZERO_ONE_ZERO 从 0 变化到 1再回到 0 QUAD_IN 开始较慢随后加速 QUAD_OUT 开始较快随后减速 QUAD_IN_OUT 开始和结束较慢 BACK_OUT 超过目标后回弹 BOUNCE_OUT 到达目标时产生弹跳 ELASTIC_OUT 带有弹性振荡效果这些曲线通过KEY_CURVE中的 ID 使用KEY_CURVE.ZERO_ONE KEY_CURVE.QUAD_OUT KEY_CURVE.BACK_OUT KEY_CURVE.BOUNCE_OUT同一条曲线可以用于位置、缩放、透明度和其他属性。三、移动一个对象让 UI 节点从左侧移动到目标位置Vector3 start new(-500.0f, 0.0f, 0.0f); Vector3 target Vector3.zero; mPanel.MOVE( KEY_CURVE.QUAD_OUT, start, target, 0.3f);参数含义KEY_CURVE.QUAD_OUT 使用的曲线 start 起始位置 target 目标位置 0.3f 动画时长不指定曲线时默认使用线性变化mPanel.MOVE( start, target, 0.3f);调用后框架会为对象添加或复用COMTransformableMove由组件自动完成后续更新。业务类不需要自己编写update()。四、缩放和透明度界面弹出时可以同时播放缩放和透明度mPanel.SCALE( KEY_CURVE.BACK_OUT, 0.7f, 1.0f, 0.25f); mPanel.ALPHA( KEY_CURVE.QUAD_OUT, 0.0f, 1.0f, 0.2f);最终效果是缩放从 0.7 回弹到 1.0 透明度从 0 渐变到 1直接设置目标值mPanel.SCALE(1.0f); mPanel.ALPHA(1.0f);这种写法会停止当前同类型缓动并立即应用新的值。如果只是想立即改变属性不要传入持续时间为0直接调用对应的单值接口即可。五、监听播放过程和完成结果需要在变化中或完成后执行逻辑可以使用带_EX的接口mPanel.MOVE_EX( KEY_CURVE.QUAD_OUT, start, target, 0.3f, onMoving, onMoveDone);回调函数protected void onMoving( ComponentKeyFrame component, bool isBreak) { // 每次应用新的位置后调用 } protected void onMoveDone( ComponentKeyFrame component, bool isBreak) { if (isBreak) { return; } log(移动完成); }isBreak表示本次缓动是否被其他操作打断。例如移动尚未结束又执行mPanel.MOVE(Vector3.zero);旧缓动的回调会收到isBreak true这样可以区分动画正常完成 动画被新操作覆盖 对象销毁导致动画中断六、同一种缓动会自动复用组件第一次执行移动mPanel.MOVE(start, target, 0.3f);框架会创建COMTransformableMove再次对同一个对象执行移动时不会继续创建新组件而是复用原来的组件并替换参数mPanel.MOVE( mPanel.getPosition(), new Vector3(300.0f, 0.0f, 0.0f), 0.5f);旧移动会被中断新移动立即开始。不同类型的缓动则可以同时存在mPanel.MOVE(start, target, 0.3f); mPanel.SCALE(0.7f, 1.0f, 0.3f); mPanel.ALPHA(0.0f, 1.0f, 0.3f);因为它们分别由不同组件驱动。七、循环播放让图标持续呼吸缩放mIcon.SCALE( KEY_CURVE.ZERO_ONE_ZERO, 0.9f, 1.1f, 0.8f, true);ZERO_ONE_ZERO会让曲线值从0 → 1 → 0所以缩放会在0.9和1.1之间往返。最后一个参数表示循环播放true停止循环并恢复正常大小mIcon.SCALE(1.0f);八、设置播放偏移多个对象使用同一条循环曲线时可以通过偏移制造错开的效果mIcon1.SCALE( KEY_CURVE.ZERO_ONE_ZERO, 0.9f, 1.1f, 0.8f, true, 0.0f); mIcon2.SCALE( KEY_CURVE.ZERO_ONE_ZERO, 0.9f, 1.1f, 0.8f, true, 0.4f);两个图标使用相同曲线但第二个从曲线中间开始播放。需要注意offset 不能大于 onceLength它表示当前曲线的起始时间而不是开始播放前的等待时间。九、创建自定义曲线除了内置曲线还可以在下面的资源中编辑自定义曲线Assets/GameResources/KeyFrame/Keyframe.prefab该 Prefab 上挂有GameKeyframe自定义曲线 ID 从101开始因为100以内由框架内置曲线使用。例如创建一条 UI 弹出曲线ID 101 Name UIPop在应用层定义常量public class GAME_KEY_CURVE { public const int UI_POP 101; }使用方式和内置曲线一致mPanel.SCALE( GAME_KEY_CURVE.UI_POP, 0.7f, 1.0f, 0.25f);KeyFrameManager初始化时会加载Keyframe.prefab并把其中的AnimationCurve加入曲线列表。这样美术或策划可以直接在 Unity 曲线面板中调整效果而不需要修改插值代码。十、渐变一个普通数值有些数据不属于任何可变换对象例如经验条数字、计数器或自定义 Shader 参数。可以使用MyTweenerFloat例如让数字从0增长到100protected MyTweenerFloat mNumberTweener; protected void playNumber() { OT.TWEEN_FLOAT(ref mNumberTweener); mNumberTweener OT.TWEEN_FLOAT( 0.0f, 100.0f, 1.0f, (value) { mNumberText.setText( ((int)value).IToS()); }); }开始新的渐变前先销毁旧对象OT.TWEEN_FLOAT(ref mNumberTweener);避免多个数值渐变同时修改同一个目标。不过这种设计其实不好,因为只在这里才会去销毁Tween对象,后续会改成缓动结束后会自动销毁Tween对象.手动销毁只用于中途需要中断的情况.TWEEN_FLOAT同样使用KeyFrameManager的曲线因此可以指定缓动方式mNumberTweener OT.TWEEN_FLOAT( KEY_CURVE.QUAD_OUT, 1.0f, onNumberChanging);十一、UI 缓动需要允许界面更新UI 缓动依赖LayoutScript的更新流程。如果当前界面没有开启更新却对 UI 节点播放缓动编辑器中会输出提示想要使窗口播放缓动动画但是窗口当前未开启更新因此需要播放持续动画的界面应确保对应 UI 对象能够被布局更新。框架在开始移动、旋转或缩放时也会自动把对象标记为需要更新setNeedUpdate(true)但所属布局本身仍然需要处于可更新状态。十二、总结MyFramework 的关键帧体系可以统一驱动位置 旋转 缩放 透明度 摄像机参数 时间缩放 普通浮点数常用调用方式包括obj.MOVE(...); obj.ROTATE(...); obj.SCALE(...); obj.ALPHA(...); OT.TIME(...); OT.TWEEN_FLOAT(...);业务代码只描述从哪里开始 变化到哪里 使用哪条曲线 需要多长时间 是否循环具体的时间累计、曲线求值、组件更新和中断回调都由ComponentKeyFrame统一处理。这样不同系统可以共用同一套缓动规则也不需要在每个类中重复实现插值和生命周期管理。