1. Tween System

Tween(补间) 是指在两个状态之间,通过时间插值自动生成中间帧(in-between),从而产生平滑过渡的动画或变化。

因为在游戏中,我们很多时候需要进行插值来表示过渡动画。比如物体从一个点移动到目的地,只需要设定初始值和目的地值,还有所用的时间,那么中间的过程也就确定了,只需要通过计算去获取每个时刻的值。

最简单的方式可以通过 Lerp 函数来进行插值,在归一化时间的表示下(0表示开始,1表示结束),每个中间时刻的值都可以通过 Lerp 插值出来。

最简易的写法肯定是每个过程都自己编写,在 Update 中计算中间过程,比如:

public bool Update(float deltaTime)
{
    if (_isCompleted) return true;

    _elapsed += deltaTime;
    float t = Mathf.Clamp01(_elapsed / _duration);
    float value = Mathf.Lerp(_startValue, _endValue, easedT);

    _onUpdate?.Invoke(value);

    _isCompleted = _elapsed >= _duration;

    return _isCompleted;
}

1.1 Tween Manager

不过这种写法肯定不是一个好的写法,更好的方式是使用“协程”来进行处理,同时我们把整个类提取出来作为一个公共单例,这样插值和调用过程就简化了。只需要通过回调参数 onUpdateonComplete 来决定中间态和结束时的回调。

public class TweenManager : MonoBehaviour
{
    private static TweenManager _instance;

    public static TweenManager Instance
    {
        get
        {
            if (_instance == null)
            {
                var go = new GameObject("TweenManager");
                _instance = go.AddComponent<TweenManager>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }

    public Coroutine TweenFloat(float from, float to, float duration, Action<float> onUpdate,
        Action onComplete = null)
    {
        return StartCoroutine(TweenFloatCoroutine(from, to, duration, onUpdate, onComplete));
    }

    private IEnumerator TweenFloatCoroutine(float from, float to, float duration, Action<float> onUpdate, Action onComplete)
    {
        float time = 0f;
        while (time < duration)
        {
            time += Time.deltaTime;
            float t = Mathf.Clamp01(time / duration);
            float value = Mathf.Lerp(from, to, t);
            onUpdate?.Invoke(value);
            yield return null;
        }
        onUpdate?.Invoke(to);
        onComplete?.Invoke();
    }
    
    public Coroutine TweenColor(Color from, Color to, float duration, Action<Color> onUpdate,
        Action onComplete = null)
    {
        return StartCoroutine(TweenColorCoroutine(from, to, duration, onUpdate, onComplete));
    }

    private IEnumerator TweenColorCoroutine(Color from, Color to, float duration, Action<Color> onUpdate, Action onComplete)
    {
        float time = 0f;
        while (time < duration)
        {
            time += Time.deltaTime;
            float t = Mathf.Clamp01(time / duration);
            Color value = Color.Lerp(from, to, t);
            onUpdate?.Invoke(value);
            yield return null;
        }
        onUpdate?.Invoke(to);
        onComplete?.Invoke();
    }
}

1.2 插值颜色和 Float

在写好了 TweenManager 以后,调用的时候只需要编写回调参数即可。

比如对颜色进行插值:

public class ColorFadeExample : MonoBehaviour
{
    public Image target;

    private void OnGUI()
    {
        if (GUI.Button(new Rect(10, 10, 100, 30), "TweenColor"))
        {
            Color start = Color.white;
            Color end = Color.red;
            float duration = 2f;
        
            TweenManager.Instance.TweenColor(start, end, duration, color => target.color = color, ()=> Debug.Log("Color Tween Done!"));
        }
    }
}

Tween Color

对浮点数进行插值,实现一个计时器:

public class TextExample : MonoBehaviour
{
    public Text target;

    private void OnGUI()
    {
        if (GUI.Button(new Rect(10, 50, 100, 30), "TweenFloat"))
        {
            float start = 0f;
            float end = 10f;
            float duration = 5f;
        
            TweenManager.Instance.TweenFloat(start, end, duration, value => target.text = $"Time: {value:F4}", ()=> Debug.Log("Float Tween Done!"));
        }
    }
}

Tween Float


2.3 利用 C# 扩展方法简化写法

扩展方法(Extension Method)是 C# 提供的一种语法糖,它允许你给现有类型添加方法,而无需修改原来的类。

也就是说,你可以“扩展”别人的类,包括 Unity 的 Image/string/List<T> 等。

语法特点:

  • 方法必须是 static
  • 第一个参数要用 this 修饰,表示这个方法扩展的是哪个类型。
  • 必须放在 static class 中。
public static class TweenManagerExtension
{
    public static Coroutine TweenColor(this Image image, Color from, Color to, float duration, Action<Color> onUpdate, Action onComplete = null)
    {
        return TweenManager.Instance.TweenColor(from, to, duration, onUpdate, onComplete);
    }
}

使用的时候就可以直接当作一个方法来使用:

    public class ColorFadeExample : MonoBehaviour
    {
        public Image target;

        private void OnGUI()
        {
            if (GUI.Button(new Rect(10, 10, 100, 30), "TweenColor"))
            {
                Color start = Color.white;
                Color end = Color.aquamarine;
                float duration = 2f;

                target.TweenColor(start, end, duration, color => target.color = color, () => Debug.Log("Color Tween Done!!"));
            }
        }
    }

这样就简化的调用方式,使得代码写起来更优雅了。

Tween Color Extension


2. DOTween 框架

在前一小节的最后一步,通过 C# 扩展方法,已经可以实现便捷的补间操作了。

但是一个问题在于,实际上还有很多游戏开发中的场景需要进行适配,比如:

  • 循环多次地播放一段动画
  • 逆向播放一段动画
  • 将多个动画连起来播放
  • 不仅仅是浮点数和颜色,还有文字等各种场景

一个好消息是,已经有人这么做了,并且性能上也进行了优化,它就是 DOTween 。

DOTween 它的全名是 Demigiant DOTween,作者是 Daniele Giardini,是目前 Unity 最主流的 Tween System 之一。

参考文档:DOTween Documentation1

2.1 DOTween 安装

安装它非常容易,就按照正常的插件安装方式,在 Unity Asset Store 中找到它并安装即可:Unity Asset Store DOTWeen

DOTween Asset Store


2.2 振动、文字、移动缩放

DOTween 不仅仅是简单的插值,也包括很多特效动画。

比如,可以振动位置来实现抖动镜头的效果:

private void OnGUI()
{
    if (GUI.Button(new Rect(10, 10, 100, 30), "Shake"))
    {
        Camera.main.transform.DOShakePosition(2, shakeStrength);
    }
}

DOTween Shake

可以逐个输出一段文字:

private void OnGUI()
{
    if (GUI.Button(new Rect(10, 50, 100, 30), "TextPrint"))
    {
        outputTextCanvas.gameObject.SetActive(true);
        outputText.text = "";
        outputText.DOText(" Hello world \n !!!!!!!!!!!!!!!!!!!!!!!!!!", 4);
    }
}

DOTween Print Text

能够将物体从当前位置移动到另一个地方。

private void OnGUI()
{
    if (GUI.Button(new Rect(10, 130, 100, 30), "MoveTo"))
    {
        targetTransform.DOMove(new Vector3(0, 2, 0), 1f);
    }
}

DOTween Move To


2.3 Sequence

移动的同时进行缩放。

这里的 Sequence 将会等待前一个动画运行完毕再开始下一个动画。

private void OnGUI()
{
    if (GUI.Button(new Rect(10, 170, 100, 30), "MoveToAndScale"))
    {
        var s = DOTween.Sequence();
        s.Append(targetTransform.DOMove(new Vector3(0, 2, 0), 1f));
        s.Append(targetTransform.DOScale(new Vector3(0.5f, 0.5f, 0.5f), 1f));
        s.OnComplete((() => Debug.Log("MoveToAndScale Done!")));
    }
}

DOTween Move And Scale


2.4 相对值、OnComplete

不仅仅可以移动绝对值,还可以移动相对值,也就是相对于动画启动的位置:

if (GUI.Button(new Rect(10, 130, 100, 30), "MoveTo"))
{
    targetTransform.DOMove(new Vector3(0, 2, 0), 1f).SetRelative();
}

DOTween Move Relative

通过组合序列,可以实现连贯的动画效果,并在结束的时候执行隐藏文本的效果:

if (GUI.Button(new Rect(10, 90, 100, 30), "TextColor"))
{
    outputTextCanvas.gameObject.SetActive(true);
    outputText.text = "";
    var s = DOTween.Sequence();
    
    // 文字打字机动画
    s.Append(outputText
        .DOText("Hello world \n !!!!!!!!!!!!!!!!!!!!!!!!!!", 4)
        .SetEase(Ease.Linear));

    // 颜色变化
    s.Append(outputText
        .DOColor(new Color(0.8f, 0.2f, 0.3f), 1f)
        .SetEase(Ease.InOutSine));

    // 闪烁:反复改变透明度
    s.AppendCallback(() =>
    {
        outputText.DOFade(0f, 0.3f)
            .SetLoops(6, LoopType.Yoyo) // 闪烁3次 Yoyo是反复模式
            .SetEase(Ease.InOutSine).OnComplete(() =>
            {
                outputTextCanvas.gameObject.SetActive(false);
            });
    });
}

alt text


2.5 取消、暂停、恢复

直接调用 Kill(),这会让补间立即停止,不再更新。

Tween tween = transform.DOMove(new Vector3(5, 0, 0), 2f);
tween.Kill();  // 立即取消并移除补间

你可以用目标对象作为参数,批量取消:

transform.DOMove(...);
transform.DOScale(...);
DOTween.Kill(transform);  // 杀掉与该 transform 相关的所有 Tween

常用于物体被销毁前:

void OnDestroy()
{
    DOTween.Kill(transform);
}

同时还可以临时暂停动画与恢复动画:

Tween t = transform.DOMove(...);
t.Pause();  // 暂停
t.Play();   // 继续

DOTween.PauseAll();
DOTween.PlayAll();

2.6 曲线、延迟、循环

补间曲线(Easing)控制动画随时间变化的速度,默认情况下动画是线性的(Linear),即速度恒定。

但通过设置不同的缓动曲线(Ease),可以获得更自然的运动效果。

transform.DOMoveY(3f, 1.5f)
    .SetEase(Ease.OutBack); // 弹性移动效果

有时我们希望动画稍后再播放,这时候通过 SetDelay 设置延迟播放时间:

image.DOFade(1f, 1f)
     .SetDelay(0.5f); // 动画在0.5秒后开始

循环定义了动画重复播放的次数与模式,通过 SetLoops(int loops, LoopType type) 实现。

  • loops:循环次数。-1 表示无限循环。
  • type:循环方式。Restart是循环,Yoyo是从起点到终点,再返回。Incremental在上一次的基础上增加。
transform.DOScale(1.5f, 0.8f)
         .SetEase(Ease.InOutSine)
         .SetLoops(3, LoopType.Yoyo);

  1. demigiant, DOTween Documentation, https://dotween.demigiant.com/documentation.php ↩︎