补间

感知暂停的数值动画,内置 31 条缓动曲线,由时间服务的时钟驱动。

让任何数值随时间从 A 动画到 B——并且天然尊重暂停菜单。

接口ITweenService
关闭开关NullTweenService
程序集CommonGameSystem.Core
启动启动时自动注册——无需任何设置

功能概览

**补间(tween)**是在固定时长内、从起始值到结束值的平滑插值。补间服务每个 tick 计算这个值;**缓动曲线(easing curve)**塑造加速度的形状(先慢后快、过冲后回落、弹跳等等)。把每个值应用到对象上的回调由你提供——框架只负责计算。没有反射,没有自动属性写入,没有外部依赖。

每个补间都绑定到时间服务的某个时钟——Gameplay、UI 或 Background——因此你的动画无需额外代码即可尊重暂停菜单与慢动作。绑定在 Gameplay 时钟上的开门补间会在玩家暂停时冻结;UI 时钟上的菜单淡入淡出则继续运行。

快速上手

AwakeStart 中解析一次该服务并缓存:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyMovingThing : MonoBehaviour
{
    private ITweenService _tween;

    private void Awake()
    {
        _tween = ServiceLocator.Resolve<ITweenService>();
    }

    private void StartMove()
    {
        _tween.To(
            from: transform.position,
            to: new Vector3(5f, 0f, 0f),
            durationSeconds: 0.5f,
            onUpdate: pos => transform.position = pos,
            ease: EaseType.OutQuad
        );
    }
}

API 参考

类型化的 To / From 重载面

该服务恰好支持六种值类型,每种都有自己的一组类型化重载:

float · Vector2 · Vector3 · Vector4 · Color · Quaternion

每种类型有四个重载——ToFrom,搭配默认时钟或显式的 Clock 参数——总计 24 个方法。它们的形态完全一致(此处以 float 为例;可替换为六种类型中的任意一种):

IDisposable To(float from, float to, float durationSeconds,
               Action<float> onUpdate,
               EaseType? ease = null,
               Func<float, float> customEase = null,
               Action onComplete = null);

IDisposable To(float from, float to, float durationSeconds, Clock clock,
               Action<float> onUpdate,
               EaseType? ease = null,
               Func<float, float> customEase = null,
               Action onComplete = null);

IDisposable From(float from, float current, float durationSeconds,
                 Action<float> onUpdate,
                 EaseType? ease = null,
                 Func<float, float> customEase = null,
                 Action onComplete = null);

IDisposable From(float from, float current, float durationSeconds, Clock clock,
                 Action<float> onUpdate,
                 EaseType? ease = null,
                 Func<float, float> customEase = null,
                 Action onComplete = null);

From(a, current, ...)To(a, current, ...) 是同一个操作——只是当你某个值动画回当前值时(例如缩放冲击效果),这个名字读起来更顺。

这里刻意不提供泛型 To<T> 方法:一组固定的类型化重载在 Unity 的提前编译(IL2CPP)下保证可用,而针对值类型的开放泛型方法在运行时可能失败。Quaternion 补间使用 Quaternion.Slerp 沿最短弧旋转。

ToFrom 都返回一个 IDisposable 令牌——释放它即可提前取消补间。

参数

  • fromto(或 current — 起始值与结束值。
  • durationSeconds — 动画时长(秒)。时长为 0 时立即在 to 处完成。
  • clock — 可选。Clock.Gameplay(默认值)随游戏一起暂停;Clock.UI 在暂停菜单期间继续运行;Clock.Background 始终运行。
  • onUpdate — 每个 tick 携带插值结果被调用。由你把它应用到对象上。
  • ease — 可选的 EaseType。省略则使用默认缓动,或从下面的 31 条曲线中选择一条。
  • customEase — 可选的 Func<float, float>,把进度(0 到 1)映射为缓动后的进度。提供后会覆盖 ease
  • onComplete — 补间到达 100% 时触发一次。取消时不会触发。

31 条缓动曲线

EaseType 覆盖 Linear 外加十个曲线族,每族各有三个方向——In(加速进入运动)、Out(减速离开运动)和 InOut(两者兼有):

曲线族成员特点
LinearLinear恒定速度,无缓动
QuadInQuad · OutQuad · InOutQuad温和的加速(平方)
CubicInCubic · OutCubic · InOutCubic中等强度的加速(立方)
QuartInQuart · OutQuart · InOutQuart强烈的加速
QuintInQuint · OutQuint · InOutQuint非常强烈的加速
SineInSine · OutSine · InOutSine柔和的、基于正弦波的缓动
ExpoInExpo · OutExpo · InOutExpo戏剧性的指数攀升
CircInCirc · OutCirc · InOutCirc圆弧轨迹,一端变化急促
BackInBack · OutBack · InOutBack越过目标后再回落安定
ElasticInElastic · OutElastic · InOutElastic弹簧般冲过目标并来回振荡
BounceInBounce · OutBounce · InOutBounce在边界处像球一样弹跳

经验法则:大多数 UI 运动用 OutQuadOutCubic;俏皮的弹入用 OutBack;卡通式的强调用 OutBounce 和 Elastic 系列。

诊断

  • int ActiveCount { get; } — 存活补间的数量。仅用于诊断;仅限主线程。

示例

在 0.3 秒内淡入一个面板,完成时打印日志:

CanvasGroup panel = GetComponent<CanvasGroup>();

_tween.To(
    from: 0f,
    to: 1f,
    durationSeconds: 0.3f,
    clock: Clock.UI, // UI clock: keep running while the pause menu is open.
    onUpdate: alpha => panel.alpha = alpha,
    ease: EaseType.OutCubic,
    onComplete: () => Debug.Log("Fade complete!")
);

通过释放令牌来取消补间:

float score = 0f;
IDisposable token = _tween.To(0f, 100f, 1f, v => score = v);
// ... later, if you want to stop early:
token.Dispose();  // The tween cancels; onComplete does NOT fire.

关闭该服务

ServiceLocator.Replace<ITweenService>(new NullTweenService());

这会静默所有补间。ToFrom 返回同一个共享的空令牌,不触发任何回调,也不创建 ticker 对象。适合在编辑器工具或自动化测试中禁用动画。空操作版本仍会校验输入:onUpdate 为 null、ClockEaseType 无效、durationSeconds 为 NaN、无穷或负数时仍会抛出异常——即使关闭了动画,bug 依然可见。

常见陷阱

  • 选对时钟。 时钟选错并不是错误,但动画的感觉会不对——例如某个游戏动画因为绑到了 Clock.UI 而在暂停期间继续运动。各时钟在暂停与慢动作下的行为见时间服务
  • Quaternion 缓动会钳制过冲。 过冲类曲线(InBackOutBackInElasticOutElastic)不会表现出可见的旋转过冲,因为 Quaternion.Slerp 会把因子钳制在 0–1 范围内。过冲阶段旋转会停留在结束值上。
  • 取消不等于完成。 释放补间令牌会静默取消它——onComplete 不触发。走完全部时长才会触发 onComplete
  • 委托分配是你这边的开销。 服务对内部补间槽做了池化,但每次调用时 onUpdate lambda(以及任何 customEase 闭包)都由你分配。对于不允许分配的紧凑循环,把委托存进字段一次,之后每次调用都传那个字段,而不是写新的 lambda。
  • 不要在补间完成后继续持有令牌。 已完成补间的槽会被后续补间复用。过期令牌会被检测并忽略,因此迟到的释放不会造成任何伤害——但持有它本身就是生命周期 bug 的征兆。即发即忘的补间根本不需要保存令牌。

相关页面

  • 时间 — 补间绑定的时钟
  • 补间序列 — 顺序与并行的补间时间线
  • 调度器 — 同一组时钟上的延迟与循环计时器