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

比方说现在有一个游戏对象,需要让它花费 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 组件类型 Tweener 和 Sequence。
namespace MyDOTween {
public enum TweenType {
Tweener,
Sequence
}
}
Sequentiable.cs
每个可以放到 Sequence 中的组件都需要定义其 Tween 类型。
namespace MyDOTween {
public abstract class Sequentiable {
internal TweenType tweenType;
}
}
Tween.cs
Tween 是 Tweener 和 Sequence 的抽象类,用 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
静态类 EaseManager 的 Evaluate 方法会根据 Ease 类型、elapsed 和 duration 来确定一个 [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,定义了 SetValues 和 EvaluateAndApply 两个抽象函数,被 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
在 DOMove 和 DOScale 函数中,变换类型是 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 静态类维护了所有活动的 Tween。GetTweener 和 GetSequence 是创建 Tween 的工厂方法,创建完成后会将 Tween 加入活动列表。每经过一个 deltaTime,TweenManager 需要更新活动列表中的所有 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 完成任务。此处实现了 Vector3 和 Color 的 To 函数。
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
该模块实现了一系列扩展方法:
-
DOMove、DOScale和DOColor分别用于移动、伸缩变换和颜色变换,他们都通过DOTween.TO实现,只是getter和setter(用 Lambda 函数定义)有所不同。 -
SetEase用于Ease类型设置。 -
Append将Tween添加至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))
);
}
}
启动程序,效果如下(此处应该配上变形金刚的音效):
