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))
);
}
}
启动程序,效果如下(此处应该配上变形金刚的音效):