DOTween 动画引擎仿写

DOTween 是一款针对 Unity 的动画引擎,DOTween 官网首页的图片很直观地展示了 DOTween 的作用:通过 Tweener 组件可以实现物体的动画效果,而 Sequence 则是组件(包括 TweenerSequence)的集合。

比方说现在有一个游戏对象,需要让它花费 5 秒钟从 (0, 0, 0) 坐标移动至 (0, 10, 0) 坐标。通过在该对象的 Transform 中调用 DOMove 函数即可实现该功能(该函数会生成一个 Tweener),无需编写 Update 函数,更不用关心 Time.deltaTime

transform.DOMove(new Vector3(0, 10, 0), 5);

如果要让移动过程呈现线性变化,可以通过 SetEase 函数设置:

transform.DOMove(new Vector3(0, 10, 0), 5).SetEase(Ease.Linear);

进一步说,如果要让该对象在移动的同时不断扩大呢?只需再调用 DOScale 函数:

transform.DOMove(new Vector3(0, 10, 0), 5);
transform.DOScale(new Vector3(2, 2, 2), 5);

同样的道理,如果想让该对象在移动过程中逐渐变成蓝色,只需要增加 DOColor 函数的调用语句(修改对象的 Material):

transform.DOMove(new Vector3(0, 10, 0), 5);
transform.DOScale(new Vector3(2, 2, 2), 5);
GetComponent<Renderer>().material.DOColor(Color.blue, 5);

这些过程都是可以设置变化曲线的:

transform.DOMove(new Vector3(0, 10, 0), 5).SetEase(Ease.InSine);
transform.DOScale(new Vector3(2, 2, 2), 5).SetEase(Ease.Linear);
GetComponent<Renderer>().material.DOColor(Color.blue, 5).SetEase(Ease.InSine);

以上的设置会让三种变换同时执行,如果要让它们按照次序执行,就需要用到 Sequence 了:

Sequence mySequence = DOTween.Sequence();
mySequence.Append(transform.DOMove(new Vector3(0, 10, 0), 5));
mySequence.Append(transform.DOScale(new Vector3(2, 2, 2), 5));
mySequence.Append(GetComponent<Renderer>().material.DOColor(Color.blue, 5));

当然,Sequence 中也可以嵌套 Sequence

Sequence mySequence = DOTween.Sequence();
mySequence.Append(transform.DOMove(new Vector3(0, 10, 0), 5));
mySequence.Append(transform.DOScale(new Vector3(2, 2, 2), 5));
mySequence.Append(GetComponent<Renderer>().material.DOColor(Color.blue, 5));
mySequence.Append(
    DOTween.Sequence()
        .Append(transform.DOMove(new Vector3(0, 0, 0), 3))
        .Append(transform.DOScale(new Vector3(0.5f, 0.5f, 0.5f), 3))
        .Append(GetComponent<Renderer>().material.DOColor(Color.white, 3))
);

以上的例子只展示了 DOTween 中的一小部分功能,更多的功能可见 DOTween 的官方文档。

DOTween 的实现

DOTween 是一个开源项目,通过查看源码就能大致了解 DOTween 的实现。由于作者需要保证引擎的性能、可用性和可扩展性,所以源码中有大量复杂的细节,而我在实现中对这些部分进行了大幅度的简化,只保留了整体的框架。下文将逐一介绍每个部件,完整项目可见 GitHub 仓库

TweenType.cs

TweenType 定义了两种 Tween 组件类型 TweenerSequence

namespace MyDOTween {
    public enum TweenType {
        Tweener,
        Sequence
    }
}

Sequentiable.cs

每个可以放到 Sequence 中的组件都需要定义其 Tween 类型。

namespace MyDOTween {
    public abstract class Sequentiable {
        internal TweenType tweenType;
    }
}

Tween.cs

TweenTweenerSequence 的抽象类,用 active 表示其活动状态,同时具有 Update 抽象函数用于更新状态。

namespace MyDOTween {
    // Tweener or Sequence
    public abstract class Tween : Sequentiable {
        // active or not?
        public bool active { get; internal set; }
        // whether or not this tween is in sequence
        internal bool isSequenced = false;

        internal abstract void Update(float deltaTime);
    }
}

Delegates.cs

该模块定义了两种委托,DOGetter 用于获得需要变换的值,DOSetter 设置需要变换的值。

namespace MyDOTween {
    // getter for tween
    public delegate T DOGetter<T>();
    // setter for tween
    public delegate void DOSetter<T>(T newValue);
}

Ease.cs

该模块定义了两种变化方式,更多变换方式可见官方文档。

namespace MyDOTween {
    public enum Ease {
        Linear,
        InSine
    }
}

EaseManager.cs

静态类 EaseManagerEvaluate 方法会根据 Ease 类型、elapsedduration 来确定一个 [0, 1] 区间内的值,该返回值将用于变换值的设定(见下文)。其中,elapsed 表示从组件开始执行到目前为止经过的时间,而 duration 组件表示该动作的持续时间(由用户定义)。

using UnityEngine;
using System;

namespace MyDOTween {
    public static class EaseManager {
        // get a value in [0, 1] based on the elapsed time and ease selected
        public static float Evaluate(Ease easeType, float elapsed, float duration) {
            switch (easeType) {
                case Ease.Linear:
                    return elapsed / duration;
                case Ease.InSine:
                    return -(float)Math.Cos((elapsed / duration) * (Mathf.PI * 0.5f)) + 1;
                default:
                    return 1;
            }
        }
    }
}

Tweener.cs

Tweener 是 DOTween 的核心组件。这是一个泛型类,TweenVT 表示需要变换的类型,StoreVT 表示实际存储的类型。在我的实现中,这两个类型都是一样的,但是在定制化的场景中可能是不同的(比方说传入的值是 Vector3 表示的坐标,但是实际变换的值是 float 类型的 x 坐标)。

这里有一个重要的成员变量 tweenPlugin,它是 TweenPlugin<TweenVT, StoreVT> 类型的插件,具体的插件对象需要通过 PluginsManager 来获取。TweenPlugin 只是一个抽象类,充当适配器的角色,通过实现该抽象类就可以让 Tweener 适配不同的变换类型(Vector3 和 Color 等)。

Update 函数会根据 deltaTime 更新 Tweener 的状态。Tweener 启动后,首先通过 tweenPlugin 设置变换的初始值以及从开始到结束的过程中需要变换的值。每经过一个 deltaTime,就更新 elapsed 累积时间,并通过 tweenPlugin.EvaluateAndApply 函数进行变换。当累积时间到达用户设置的 duration 后,结束变换。

using System;

namespace MyDOTween {
    // TweenVT: type of value to tween
    // StoreVT: format in which value is stored while tweening
    public class Tweener<TweenVT, StoreVT> : Tween {
        // start or not
        internal bool start;

        // getter for tweener
        public DOGetter<TweenVT> getter = null;
        // setter for tweener
        public DOSetter<TweenVT> setter = null;
        // tween plugin (for current TweenVT and StoreVT)
        internal TweenPlugin<TweenVT, StoreVT> tweenPlugin;

        // start value
        public StoreVT startValue;
        // end value
        public StoreVT endValue;
        // the distance from start value to end value
        public StoreVT changeValue;

        // elapsed time
        public float elapsed;
        // duration time
        public float duration;

        // ease type
        internal Ease easeType;

        // constructor
        internal Tweener() {
            tweenType = TweenType.Tweener;
        }

        internal bool Setup(
            DOGetter<TweenVT> getter, DOSetter<TweenVT> setter,
            StoreVT endValue, float duration
        ) {
            // not start yet
            this.start = false;
            // set getter & setter
            this.getter = getter;
            this.setter = setter;
            // set tween plugin
            if (tweenPlugin == null) {
                tweenPlugin = PluginsManager.GetDefaultPlugin<TweenVT, StoreVT>();
            }
            // set end value
            this.endValue = endValue;
            // set time
            this.elapsed = 0;
            this.duration = duration;
            // set ease type
            this.easeType = DOTween.defaultEaseType;
            return true;
        }

        internal override void Update(float deltaTime) {
            if (!active) {
                return;
            }
            if (!start) {
                // set start value and change value dynamically
                tweenPlugin.SetValues(this);
                start = true;
            }
            // update elapsed time
            elapsed = Math.Min(elapsed + deltaTime, duration);
            if (elapsed == duration) {
                active = false;
            }
            // set new value
            tweenPlugin.EvaluateAndApply(this, setter, elapsed, duration, startValue, changeValue);
        }
    }
}

ITweenPlugin.cs

ITweenPlugin 是一个简单的插件接口。

namespace MyDOTween {
    public interface ITweenPlugin {}
}

TweenPlugin.cs

TweenPlugin 抽象类实现了 ITweenPlugin,定义了 SetValuesEvaluateAndApply 两个抽象函数,被 Tweener 使用(见上文)。

namespace MyDOTween {
    public abstract class TweenPlugin<TweenVT, StoreVT> : ITweenPlugin {
        // set tweener's startValue and changeValue
        public abstract void SetValues(Tweener<TweenVT, StoreVT> tweener);

        // evaluate and apply to tweener
        public abstract void EvaluateAndApply(
            Tweener<TweenVT, StoreVT> tweener, DOSetter<TweenVT> setter,
            float elapsed, float duration,
            StoreVT startValue, StoreVT changeValue
        );
    }
}

Vector3Plugin.cs

DOMoveDOScale 函数中,变换类型是 Vector3,所以此处定义 Vector3 的插件。Tweener 动作的开始值通过 getter 获取,而变换值的更新是通过 setter 设置的。

每一次变换的新值由 EaseManager.Evaluate 函数确定,与 Ease 类型相关。

using UnityEngine;

namespace MyDOTween {
    public class Vector3Plugin : TweenPlugin<Vector3, Vector3> {
        // set tweener's startValue and changeValue
        public override void SetValues(Tweener<Vector3, Vector3> tweener) {
            tweener.startValue = tweener.getter();
            tweener.changeValue = tweener.endValue - tweener.startValue;
        }

        // evaluate and apply to tweener
        public override void EvaluateAndApply(
            Tweener<Vector3, Vector3> tweener, DOSetter<Vector3> setter,
            float elapsed, float duration,
            Vector3 startValue, Vector3 changeValue
        ) {
            // get ease value
            float easeVal = EaseManager.Evaluate(tweener.easeType, elapsed, duration);
            // set new value
            setter(startValue + changeValue * easeVal);
        }
    }
}

ColorPlugin.cs

ColorPlugin 插件适配的是 Color 类型(用于 DOColor 函数),与 Vector3Plugin 基本一致。

using UnityEngine;

namespace MyDOTween {
    public class ColorPlugin : TweenPlugin<Color, Color> {
        // set tweener's startValue and changeValue
        public override void SetValues(Tweener<Color, Color> tweener) {
            tweener.startValue = tweener.getter();
            tweener.changeValue = tweener.endValue - tweener.startValue;
        }

        // evaluate and apply to tweener
        public override void EvaluateAndApply(
            Tweener<Color, Color> tweener, DOSetter<Color> setter,
            float elapsed, float duration,
            Color startValue, Color changeValue
        ) {
            // get ease value
            float easeVal = EaseManager.Evaluate(tweener.easeType, elapsed, duration);
            // set new value
            setter(startValue + changeValue * easeVal);
        }
    }
}

PluginsManager.cs

PluginsManager 管理各种适配的插件,通过调用泛型函数 GetDefaultPlugin 即可获取相应的插件。

using UnityEngine;
using System;

namespace MyDOTween {
    internal static class PluginsManager {
        // get default plugin
        internal static TweenPlugin<TweenVT, StoreVT> GetDefaultPlugin<TweenVT, StoreVT>() {
            Type tweenVT = typeof(TweenVT);
            Type storeVT = typeof(StoreVT);

            ITweenPlugin plugin = null;
            if (tweenVT == typeof(Vector3) && storeVT == typeof(Vector3)) {
                plugin = new Vector3Plugin();
            }
            else if (tweenVT == typeof(Color) && storeVT == typeof(Color)) {
                plugin = new ColorPlugin();
            }

            if (plugin != null) {
                return plugin as TweenPlugin<TweenVT, StoreVT>;
            }
            else {
                return null;
            }
        }
    }
}

Sequence.cs

DOTween 的另一个核心组件是 Sequence,它用于组织 Tween 集合。为简单起见,此处用队列保存 Tween。每增加一个 Tween,就将其加入队尾。而每经过一个 deltaTime,就调用队首 Tween 元素的 Update 函数。当队首 Tween 的变换执行完毕后,就将其移出队列,继续处理下一个 Tween

using System.Collections.Generic;

namespace MyDOTween {
    public class Sequence : Tween {
        // sequenced tweens
        internal readonly Queue<Tween> sequencedTweens = new Queue<Tween>();

        // constructor
        internal Sequence() {
            tweenType = TweenType.Sequence;
        }

        // insert tween to sequence
        internal static Sequence DoInsert(Sequence sequence, Tween tween) {
            TweenManager.AddActiveTweenToSequence(tween);
            tween.isSequenced = true;
            sequence.sequencedTweens.Enqueue(tween);
            return sequence;
        }

        internal override void Update(float deltaTime) {
            if (!active) {
                return;
            }
            if (sequencedTweens.Count > 0) {
                // get the first element in the sequenced tweens
                Tween currentTween = sequencedTweens.Peek();
                // update current tween
                currentTween.Update(deltaTime);
                // check if current tween finished
                if (!currentTween.active) {
                    sequencedTweens.Dequeue();
                }
            }
            // whether the sequence is active
            active = (sequencedTweens.Count > 0);
        }
    }
}

TweenManager.cs

TweenManager 静态类维护了所有活动的 TweenGetTweenerGetSequence 是创建 Tween 的工厂方法,创建完成后会将 Tween 加入活动列表。每经过一个 deltaTimeTweenManager 需要更新活动列表中的所有 Tween,并在最后将变换完成的 Tween 移出活动列表。

using System.Collections.Generic;

namespace MyDOTween {
    internal static class TweenManager {
        // active tweens
        internal static List<Tween> activeTweens = new List<Tween>();

        // get a new tweener
        internal static Tweener<TweenVT, StoreVT> GetTweener<TweenVT, StoreVT>() {
            Tweener<TweenVT, StoreVT> tweener = new Tweener<TweenVT, StoreVT>();
            AddActiveTween(tweener);
            return tweener;
        }

        // get a new sequence
        internal static Sequence GetSequence() {
            Sequence sequence = new Sequence();
            AddActiveTween(sequence);
            return sequence;
        }

        // whether there're active tweens
        internal static bool hasActiveTweens() {
            return activeTweens.Count > 0;
        }

        private static void AddActiveTween(Tween tween) {
            // set active
            tween.active = true;
            // add to active list
            activeTweens.Add(tween);
        }

        internal static void AddActiveTweenToSequence(Tween tween) {
            RemoveActiveTween(tween);
        }

        private static void RemoveActiveTween(Tween tween) {
            // remove from active list
            activeTweens.Remove(tween);
        }

        internal static void Update(float deltaTime) {
            // if some of the tweens are inactive, they need to be killed
            bool willKill = false;
            foreach (Tween tween in activeTweens) {
                if (!tween.active) {
                    willKill = true;
                }
                else {
                    // update tween's state
                    tween.Update(deltaTime);
                    if (!tween.active) {
                        willKill = true;
                    }
                }
            }
            // clear all inactive tweens
            if (willKill) {
                activeTweens.RemoveAll(t => !t.active);
            }
        }
    }
}

DOTweenComponent.cs

DOTweenComponent 是唯一一个继承了 MonoBehaviour 的类。Create 函数会创建一个主游戏对象,并为该对象添加 DOTweenComponent。它的 Update 函数是整个 DOTween 的主循环。在该主循环中,需要检查 TweenManager 中是否有活动的 Tween,如果有则调用 TweenManager.Update 更新所有 Tween

using UnityEngine;

namespace MyDOTween {
    public class DOTweenComponent : MonoBehaviour {
        internal static void Create() {
            if (DOTween.instance == null) {
                GameObject main = new GameObject("[DOTween]");
                DontDestroyOnLoad(main);
                DOTween.instance = main.AddComponent<DOTweenComponent>();
            }
        }

        internal static void DestroyInstance() {
            if (DOTween.instance != null) {
                Destroy(DOTween.instance.gameObject);
            }
            DOTween.instance = null;
        }

        // main loop
        private void Update() {
            if (TweenManager.hasActiveTweens()) {
                TweenManager.Update(Time.deltaTime);
            }
        }
    }
}

DOTween.cs

DOTween 定义了针对用户的接口。用户可通过调用 Init 函数进行全局的初始化(创建 DOTweenComponent),定义默认的 Ease 类型。如果用户没有调用 Init,则当有 Tween 被创建时通过 AutoInit 函数自动调用。

ApplyTo 泛型函数根据用户的设定创建 Tweener 并进行初始化。

To 函数是具体的适配函数,通过调用 ApplyTo 完成任务。此处实现了 Vector3ColorTo 函数。

Sequence 是带有全局初始化检查的工厂函数,通过 TweenManager.GetSequence 函数实现。

using UnityEngine;

namespace MyDOTween {
    public static class DOTween {
        // main instance
        public static DOTweenComponent instance;

        // default ease type
        public static Ease defaultEaseType = Ease.Linear;

        // whether or not DOTween is initialized
        internal static bool initialized = false;

        // =========================== [Init] ===========================

        public static void Init(Ease? easeTypeByDefault = null) {
            if (initialized) {
                return;
            }
            if (easeTypeByDefault != null) {
                // assign setting
                DOTween.defaultEaseType = (Ease)easeTypeByDefault;
            }
            // create main instance
            DOTweenComponent.Create();
            initialized = true;
        }

        private static void AutoInit() {
            Init(null);
        }

        private static void InitCheck() {
            if (!initialized) {
                AutoInit();
            }
        }

        // =========================== [Apply To] ===========================

        private static Tweener<TweenVT, StoreVT> ApplyTo<TweenVT, StoreVT>(
            DOGetter<TweenVT> getter, DOSetter<TweenVT> setter,
            StoreVT endValue, float duration
        ) {
            // check init
            InitCheck();
            // create tweener
            Tweener<TweenVT, StoreVT> tweener = TweenManager.GetTweener<TweenVT, StoreVT>();
            // setup tweener
            bool setupOk = tweener.Setup(getter, setter, endValue, duration);
            if (setupOk) {
                return tweener;
            }
            else {
                return null;
            }
        }

        // =========================== [To] ===========================

        // Vector3
        public static Tweener<Vector3, Vector3> TO(
            DOGetter<Vector3> getter, DOSetter<Vector3> setter,
            Vector3 endValue, float duration
        ) {
            return ApplyTo<Vector3, Vector3>(getter, setter, endValue, duration);
        }

        // Color
        public static Tweener<Color, Color> TO(
            DOGetter<Color> getter, DOSetter<Color> setter,
            Color endValue, float duration
        ) {
            return ApplyTo<Color, Color>(getter, setter, endValue, duration);
        }

        // =========================== [Sequence] ===========================

        public static Sequence Sequence() {
            InitCheck();
            Sequence sequence = TweenManager.GetSequence();
            return sequence;
        }
    }
}

Extensions.cs

该模块实现了一系列扩展方法:

  • DOMoveDOScaleDOColor 分别用于移动、伸缩变换和颜色变换,他们都通过 DOTween.TO 实现,只是 gettersetter(用 Lambda 函数定义)有所不同。

  • SetEase 用于 Ease 类型设置。

  • AppendTween 添加至 Sequence

using UnityEngine;

namespace MyDOTween {
    public static class Extensions {
        // =========================== [Do Action] ===========================

        public static Tweener<Vector3, Vector3> DOMove(
            this Transform target, Vector3 endValue, float duration
        ) {
            return DOTween.TO(() => target.position, p => target.position = p, endValue, duration);
        }

        public static Tweener<Vector3, Vector3> DOScale(
            this Transform target, Vector3 endValue, float duration
        ) {
            return DOTween.TO(() => target.localScale, s => target.localScale = s, endValue, duration);
        }

        public static Tweener<Color, Color> DOColor(
            this Material target, Color endValue, float duration
        ) {
            return DOTween.TO(() => target.color, c => target.color = c, endValue, duration);
        }

        // =========================== [For Tweener] ===========================

        public static Tweener<TweenVT, StoreVT> SetEase<TweenVT, StoreVT>(
            this Tweener<TweenVT, StoreVT> tweener, Ease ease
        ) {
            if (tweener != null && tweener.active) {
                tweener.easeType = ease;
            }
            return tweener;
        }

        // =========================== [For Sequence] ===========================

        public static Sequence Append(this Sequence sequence, Tween tween) {
            if (sequence == null || !sequence.active) {
                return sequence;
            }
            if (tween == null || !tween.active) {
                return sequence;
            }
            Sequence.DoInsert(sequence, tween);
            return sequence;
        }
    }
}

测试结果

至此,一个简单的 DOTween 已经完成,可以写个脚本测试一下。

在 Unity 中创建一个 Cube,将位置设为 (0, 0, 0),并为其添加 Test.cs 脚本:

using UnityEngine;
using MyDOTween;

public class Test : MonoBehaviour {
    void Start() {
        DOTween.Init(Ease.Linear);
        transform.DOMove(new Vector3(0, 5, 0), 3).SetEase(Ease.InSine);
    }
}

启动程序,效果如下:

修改 Test.cs 脚本,让三个变换同时进行:

using UnityEngine;
using MyDOTween;

public class Test : MonoBehaviour {
    void Start() {
        transform.DOMove(new Vector3(0, 5, 0), 3);
        transform.DOScale(new Vector3(3, 3, 3), 3);
        GetComponent<Renderer>().material.DOColor(Color.blue, 3);
    }
}

启动程序,效果如下:

修改 Test.cs 脚本,用 Sequence 实现串型变换:

using UnityEngine;
using MyDOTween;

public class Test : MonoBehaviour {
    void Start() {
        Sequence mySequence = DOTween.Sequence();
        mySequence.Append(transform.DOMove(new Vector3(0, 5, 0), 1));
        mySequence.Append(transform.DOScale(new Vector3(2, 2, 2), 1));
        mySequence.Append(GetComponent<Renderer>().material.DOColor(Color.blue, 1));
    }
}

启动程序,效果如下:

再次修改 Test.cs 脚本,尝试一下嵌套 Sequence

using UnityEngine;
using MyDOTween;

public class Test : MonoBehaviour {
    void Start() {
        Sequence mySequence = DOTween.Sequence();
        mySequence.Append(transform.DOMove(new Vector3(0, 5, 0), 1));
        mySequence.Append(transform.DOScale(new Vector3(2, 2, 2), 1));
        mySequence.Append(GetComponent<Renderer>().material.DOColor(Color.blue, 1));
        mySequence.Append(
            DOTween.Sequence()
                .Append(transform.DOMove(new Vector3(0, 1, 0), 1))
                .Append(GetComponent<Renderer>().material.DOColor(Color.white, 1))
                .Append(transform.DOScale(new Vector3(0.5f, 0.5f, 0.5f), 1))
        );
    }
}

启动程序,效果如下(此处应该配上变形金刚的音效):

Updated: