Unity 项目里做 Tween 动画,很多人第一时间会想到 DOTween。
这很正常。
DOTween 是非常成熟的 Unity Tween 引擎,常见写法很简单:
bash
transform.DOMove(targetPos, 0.3f);
transform.DOScale(Vector3.one, 0.3f);
transform.DORotate(targetRot, 0.3f);
如果要做组合动画,可以使用 Sequence:
bash
Sequence sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(targetPos, 0.3f));
sequence.Join(transform.DOScale(Vector3.one, 0.3f));
sequence.AppendInterval(0.2f);
sequence.Append(transform.DOLocalMove(originPos, 0.3f));
DOTween 官方文档里也明确提供了 Sequence,并且支持 Append、Join、Insert 等方式组织多个 Tween。Insert 可以让 Tween 在指定时间点播放,Join 可以让 Tween 和前一个 Tween 同时播放。
MyFramework 里也有一套自己的 TweenSequence。
但它的定位和 DOTween 不一样。
MyFramework 的 TweenSequence 不是为了重新实现一个通用 Tween 引擎。
它更像是框架内部的一套序列动画配置组件。
它服务的是:
- 框架内 UI 动画
- 框架内对象动画
- Inspector 配置
- 编辑器预览
- 框架组件生命周期
Transformable统一位置、缩放、旋转控制- 项目里固定动画规则
所以这篇文章不是要说谁更强。
而是具体说清楚:
MyFramework 的 TweenSequence 和 DOTween 到底有什么区别。
项目地址:
一、定位不同:通用 Tween 引擎 vs 框架内动画序列组件
DOTween 的定位是通用 Tween 引擎。
它面向的是整个 Unity 项目。
你可以用它处理:
- Transform 动画
- UI 动画
- 材质动画
- 颜色动画
- 数值动画
- 路径动画
- 回调
- 延迟
- 循环
- Sequence 拼接
它的优势是功能非常完整,API 也非常丰富。
典型 DOTween 写法是代码驱动:
bash
transform.DOLocalMove(new Vector3(100.0f, 0.0f, 0.0f), 0.3f)
.SetEase(Ease.OutQuad)
.OnComplete(onMoveDone);
如果要组合动画,就写:
bash
Sequence sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(targetPos, 0.3f));
sequence.Join(transform.DOScale(Vector3.one, 0.3f));
sequence.SetLoops(-1);
MyFramework 的 TweenSequence 不是这种定位。
它不是让业务层到处写链式 Tween 代码。
它更偏向:
把一个动画序列挂在对象上,然后由框架组件播放。
真实代码里,TweenSequence 是一个 MonoBehaviour:
bash
public class TweenSequence : MonoBehaviour
{
public List<TweenGroup> mGroupList = new();
public bool mLoop;
private Vector3 mOriginPos;
private Vector3 mOriginScale;
private Vector3 mOriginRot;
private bool mNeedResetOrigin; // 是否需要重置原始位置、缩放和旋转,当播放过以后就需要重置
public bool mResetWhenStop; // 停止播放时是否重置到原始位置、缩放和旋转
}
它本身挂在 Unity 对象上。
动画内容通过 mGroupList 配置。
播放时不是直接调用 DOTween API,而是由框架组件 COMTransformableSequence 驱动。
所以两者最核心的区别是:
DOTween 是通用 Tween 引擎。
TweenSequence 是 MyFramework 框架里的可配置动画序列组件。
二、组织方式不同:DOTween 用 Append / Join,TweenSequence 用 Group / Track
DOTween 组织 Sequence 的方式是代码链式调用。
比如顺序播放:
bash
Sequence sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(pos0, 0.3f));
sequence.Append(transform.DOScale(scale0, 0.2f));
同时播放:
bash
Sequence sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(pos0, 0.3f));
sequence.Join(transform.DOScale(scale0, 0.3f));
插入到指定时间:
bash
sequence.Insert(0.5f, transform.DORotate(rot, 0.3f));
这种方式非常灵活。
代码里可以动态决定添加哪些 Tween,执行顺序也很自由。
MyFramework 的 TweenSequence 是另一种组织方式。
它不是用 Append、Join 这种链式代码组织,而是用:
TweenSequenceTweenGroupTweenTrack
TweenSequence 里有多个 TweenGroup:
bash
public List<TweenGroup> mGroupList = new();
每个 TweenGroup 里有多个 TweenTrack:
bash
[Serializable]
public class TweenGroup
{
public List<TweenTrack> mTrackList = new();
public float getGroupLength()
{
float length = 0.0f;
foreach (TweenTrack track in mTrackList)
{
if (!track.mEnable)
{
continue;
}
length += track.mDuration;
length += track.mStartDelay;
}
return length;
}
}
这里的结构很直观:
一个 Sequence 里有多个 Group。
一个 Group 里有多个 Track。
每个 Track 有自己的:
- 类型
- 曲线
- 持续时间
- 开始延迟
- 起始值
- 目标值
- 是否启用
- 目标模式
- 起始模式
真实代码里,TweenTrack 的字段是这样的:
bash
[Serializable]
public class TweenTrack
{
protected MyCurve mCurve; // 用于在运行时缓存曲线对象
protected Vector3 mRuntimeStart; // 缓存的起始值,避免在START_MODE.CURRENT模式下每次都要获取目标对象的当前值导致错误
protected Vector3 mRuntimeTarget; // 轨道开始播放时的目标值
protected bool mPlaying; // 是否正在播放
protected float mBeginTime; // 轨道的开始时间,由TweenSequence在buildTimeline()时设置
protected float mEndTime; // 轨道的结束时间,由TweenSequence在buildTimeline()时设置
public TARGET_MODE mTargetMode = TARGET_MODE.VALUE; // 目标值的获取方式
public START_MODE mStartMode = START_MODE.VALUE; // 起始值的获取方式
public Transform mTargetTransform; // 参考的目标节点,TRANSFORM模式
public Vector3 mTargetOffset; // 是否加偏移
public TWEEN_TYPE mType; // 轨道类型,如位置、缩放、旋转等
public int mCurveID; // 曲线ID,用于在运行时获取曲线对象
public float mDuration = 0.3f; // 持续时间
public float mStartDelay; // 开始前的延迟时间
public Vector3 mStartValue; // 起始值,在编辑器中设置,运行时根据目标对象的当前值进行调整
public Vector3 mTargetValue; // 目标值,用于VALUE模式
public bool mEnable = true; // 轨道是否启用
}
DOTween 的组织方式偏代码动态构建。
TweenSequence 的组织方式偏数据配置。
这是两者很大的区别。
三、能力边界不同:DOTween 很全,TweenSequence 只做框架需要的变换
DOTween 的能力非常丰富。
它不只支持 Transform,还支持很多 Unity 对象和属性。
常见的有:
- Move
- Rotate
- Scale
- Color
- Fade
- Text
- Material
- Path
- Value Tween
- Callback
- Sequence
- Loop
- Ease
它适合做各种类型的 Tween 动画。
MyFramework 的 TweenSequence 当前能力更收敛。
它主要处理三类变换:
bash
// 缓动的类型
public enum TWEEN_TYPE : byte
{
MOVE, // 平移
ROTATE, // 旋转
SCALE, // 缩放
}
也就是说,它主要围绕 Transform 的三个核心属性:
- 位置
- 缩放
- 旋转
这不是因为做不了别的,而是设计目标不同。
MyFramework 的 TweenSequence 主要服务框架里的 UI 和对象动画。
这些动画里最常用的就是:
- UI 移动
- UI 缩放
- UI 旋转
- 节点弹出
- 节点收起
- 对象位移动画
- 对象缩放动画
- 简单循环动画
所以它没有追求 DOTween 那种完整通用能力。
它更像是:
为了框架内高频动画场景做的一套轻量配置方案。
这也是两者的取舍差异。
DOTween 适合"我想 Tween 任意属性"。
TweenSequence 适合"我想在框架规则内配置一段位置、缩放、旋转序列"。
四、播放入口不同:DOTween 返回 Tween 句柄,TweenSequence 接入 COMTransformableSequence
DOTween 播放时,一般会返回 Tween 或 Sequence 对象。
比如:
bash
Tween tween = transform.DOLocalMove(targetPos, 0.3f);
tween.Kill();
或者:
bash
Sequence sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(targetPos, 0.3f));
sequence.Kill();
所以 DOTween 的生命周期通常由 Tween / Sequence 句柄控制。
MyFramework 的播放入口不是这样。
它通过框架命令 CmdTransformableSequence 播放。
真实代码:
bash
// 缓动序列
public class CmdTransformableSequence
{
public static void execute(ITransformable obj, SequenceCallback doneCallback)
{
if (obj == null)
{
return;
}
if (isEditor() &&
obj is myUGUIObject uiObj &&
!uiObj.getLayout().canUIObjectUpdate(uiObj))
{
logError("想要使窗口播放缓动动画,但是窗口当前未开启更新:" + uiObj.getName());
}
obj.getOrAddComponent(out COMTransformableSequence com);
com.setDoneCallback(doneCallback);
com.setActive(true);
com.play(obj.tryGetUnityComponent<TweenSequence>());
// 需要启用组件更新时,则开启组件拥有者的更新,后续也不会再关闭
obj.setNeedUpdate(true);
}
public static void execute(ITransformable obj)
{
if (obj == null)
{
return;
}
obj.getComponent(out COMTransformableSequence com);
if (com == null || !com.isActive())
{
return;
}
com.stop(true);
}
}
这段代码说明了 MyFramework 的特点。
业务层不是直接操作 Tween 对象。
而是:
- 传入一个
ITransformable - 获取或添加
COMTransformableSequence - 从 Unity 对象上获取
TweenSequence - 让组件开始播放
- 自动开启对象更新
真正播放的是 COMTransformableSequence:
bash
// 用于播放物体的缓动序列
public class COMTransformableSequence : GameComponent
{
protected SequenceCallback mDoneCallback; // 序列播放完成的回调,参数1:当前组件,参数2:是否被打断
protected TweenSequence mSequence; // 当前正在播放的缓动序列
protected float mCurrentTime; // 从上一次从头开始播放到现在的时长
protected float mTotalLength; // 序列的总长度,即所有TweenGroup中最长的长度
protected PLAY_STATE mPlayState; // 播放状态
}
更新时由框架组件推进时间:
bash
public override void update(float elapsedTime)
{
base.update(elapsedTime);
if (mPlayState == PLAY_STATE.PLAY)
{
mCurrentTime += elapsedTime;
mSequence.evaluateSequence(mCurrentTime, out Vector3 pos, out Vector3 scale, out Vector3 rotation);
var transformable = mComponentOwner as Transformable;
transformable.setPosition(pos);
transformable.setScale(scale);
transformable.setRotation(rotation);
// 是否结束播放
if (mCurrentTime >= mTotalLength)
{
if (mSequence.mLoop)
{
mCurrentTime -= mTotalLength;
}
else
{
stop(false);
}
}
}
}
这就是 MyFramework 的设计重点:
TweenSequence 不自己变成一个到处传递的 Tween 句柄。
它被框架组件接管,并通过 Transformable 生命周期播放。
这样做的好处是它可以和 MyFramework 的对象组件系统统一。
UI 对象、场景对象、框架 Transformable 对象,都可以走同一套组件更新流程。
五、起点和终点模式不同:TweenSequence 更强调配置时和运行时的组合
DOTween 里常见写法是直接传目标值:
bash
transform.DOLocalMove(targetPos, 0.3f);
如果要相对移动,可以使用 DOTween 的相对模式,例如 SetRelative()。
MyFramework 的 TweenTrack 里,起点和终点有自己的模式。
起始值模式:
bash
// 缓动的起始值的类型
public enum START_MODE : byte
{
VALUE, // 编辑器配置的固定值
SELF, // 播放时取节点当前值
}
目标值模式:
bash
// 缓动的目标类型
public enum TARGET_MODE : byte
{
VALUE, // 固定值
TRANSFORM_REALTIME, // 指定节点,并且在移动过程中实时获取节点的值进行调整,适用于目标对象会移动的情况
TRANSFORM_SNAPSHOT, // 指定节点,但是只在开始移动时获取节点的值进行调整,适用于目标对象不会移动的情况
SELF, // 指定节点,但是只在开始移动时获取节点的值进行调整,适用于目标对象不会移动的情况
}
这里很有框架味道。
它把常见动画需求分成了几类:
第一,固定值。
比如从配置的 A 点移动到配置的 B 点。
第二,播放时取自己当前值。
比如 UI 当前在哪里,就从哪里开始弹出。
第三,目标节点快照。
比如播放开始时取某个目标节点的位置,之后不再变化。
第四,目标节点实时值。
比如目标对象会移动,动画过程中持续追踪目标位置。
TweenTrack.getTargetValue 就是按这个规则取目标值:
bash
public Vector3 getTargetValue(Transform transform)
{
switch (mTargetMode)
{
case TARGET_MODE.VALUE: return multiVector3(getParentAnchorScale(transform), mTargetValue);
case TARGET_MODE.TRANSFORM_REALTIME: return generateTargetValue(mTargetTransform);
case TARGET_MODE.TRANSFORM_SNAPSHOT: return mRuntimeTarget;
case TARGET_MODE.SELF: return mRuntimeTarget;
}
Debug.LogError("Unsupported tween type:" + mType);
return Vector3.zero;
}
播放开始时会缓存运行时起点和目标点:
bash
// 开始播放
public void play(Transform transform)
{
mPlaying = true;
// 起点只能在开始播放时获取,终点可以实时获取
switch (mStartMode)
{
case START_MODE.VALUE: mRuntimeStart = multiVector3(getParentAnchorScale(transform), mStartValue); break;
case START_MODE.SELF: mRuntimeStart = getTransformValue(transform); break;
}
if (mTargetMode == TARGET_MODE.TRANSFORM_SNAPSHOT)
{
mRuntimeTarget = generateTargetValue(mTargetTransform);
}
else if (mTargetMode == TARGET_MODE.SELF)
{
mRuntimeTarget = generateTargetValue(transform);
}
}
这说明 TweenSequence 的重点不是链式 API 灵活拼装。
它更强调:
动画可以配置好,但播放时仍然能根据当前对象状态和目标节点状态计算起点和终点。
这对 UI 很有用。
比如一个界面元素可能因为分辨率、适配、父节点缩放不同,最终位置不一定固定。
如果起点、终点完全写死,适配会比较麻烦。
TweenSequence 里还会处理 ScaleAnchor:
bash
protected static Vector3 getParentAnchorScale(Transform transform)
{
Transform parent = transform.parent;
if (parent != null && parent.TryGetComponent(out ScaleAnchor anchor))
{
return getScreenScale(anchor.mAspectBase);
}
return Vector3.zero;
}
这说明它和框架 UI 适配体系也有关系。
DOTween 更通用。
TweenSequence 更项目化。
六、预览方式不同:TweenSequence 有自己的 Inspector 编辑和预览
DOTween 主要是代码驱动。
当然也可以配合第三方工具或自己写编辑器,但 DOTween 本身最常见的使用方式还是代码调用。
MyFramework 的 TweenSequence 本身带 Inspector 编辑器。
TweenSequenceAuthoringEditor 是它的自定义 Inspector:
bash
[CustomEditor(typeof(TweenSequence))]
[CanEditMultipleObjects]
public class TweenSequenceAuthoringEditor : GameInspector
{
private TweenSequence mSequence;
private bool mPlaying;
private float mPreviewTime;
private double mStartTime;
}
编辑器里可以添加 Group:
bash
if (button("Add Group"))
{
Undo.RecordObject(mSequence, "Add Group");
mSequence.mGroupList.Add(new TweenGroup());
EditorUtility.SetDirty(mSequence);
}
可以添加 Track:
bash
if (button("Add Track"))
{
Undo.RecordObject(mSequence, "Add Track");
group.mTrackList.Add(new TweenTrack());
}
可以选择类型、曲线、持续时间、延迟、起始值、目标值。
比如这里显示 Track 的核心配置:
bash
toggle("Enable", ref track.mEnable);
displayEnum("Type", ref track.mType);
int[] ids = EditorCurveFactory.getIDs();
if (ids.Length == 0)
{
return;
}
if (track.mCurveID == 0)
{
track.setCurveID(ids[0]);
}
ids.find(track.mCurveID, out int curIndex);
int newIndex = EditorGUILayout.Popup("Curve", curIndex, EditorCurveFactory.getNames());
track.setCurveID(ids[newIndex]);
EditorGUILayout.CurveField("Preview", EditorCurveFactory.getPreviewCurve(track.mCurveID), GUILayout.Height(20));
displayFloat("Duration", ref track.mDuration);
displayFloat("StartDelay", ref track.mStartDelay);
它还支持在编辑器里预览。
点击播放时:
bash
private void StartPlay()
{
mPlaying = true;
// 播放之前先确认所有轨道都是在停止状态的
mSequence.stop(true);
mSequence.play();
mStartTime = EditorApplication.timeSinceStartup;
EditorApplication.update -= UpdatePreview;
EditorApplication.update += UpdatePreview;
}
预览更新时会直接调用:
bash
mSequence.evaluateSequence(currentTime, out Vector3 pos, out Vector3 scale, out Vector3 rot);
Transform transform = mSequence.transform;
transform.localPosition = pos;
transform.localScale = scale;
transform.localEulerAngles = rot;
这点和 DOTween 的使用习惯不同。
TweenSequence 的动画可以提前挂在对象上,在 Inspector 里调整和预览。
DOTween 更适合代码里临时组合、动态创建 Tween。
所以这里的区别是:
DOTween 更像程序式动画工具。
TweenSequence 更像框架内可配置动画组件。
七、停止和还原逻辑不同:TweenSequence 内置原始 Transform 记录
DOTween 停止动画时,通常会通过 Tween 或 Sequence 句柄调用:
bash
tween.Kill();
sequence.Kill();
是否回到初始状态,需要自己处理,或者使用其他方式组合。
MyFramework 的 TweenSequence 内部会记录播放前的位置、缩放、旋转。
播放时记录:
bash
public void play()
{
if (mLoop && hasSelfValueType())
{
logError("存在SELF模式轨道时不允许循环播放");
mLoop = false;
}
// 开始播放时设置每个Track的开始时间和结束时间
foreach (TweenGroup group in mGroupList)
{
float time = 0.0f;
foreach (TweenTrack track in group.mTrackList)
{
if (!track.mEnable)
{
continue;
}
time += track.mStartDelay;
track.setBeginTime(time);
time += track.mDuration;
track.setEndTime(time);
}
}
mOriginPos = transform.localPosition;
mOriginScale = transform.localScale;
mOriginRot = transform.localEulerAngles;
mNeedResetOrigin = true;
}
停止时可以还原:
bash
public void stop(bool forceReset = false)
{
foreach (var group in mGroupList)
{
foreach (var track in group.mTrackList)
{
track.stop();
}
}
if (mNeedResetOrigin && (mResetWhenStop || forceReset))
{
transform.localPosition = mOriginPos;
transform.localScale = mOriginScale;
transform.localEulerAngles = mOriginRot;
mNeedResetOrigin = false;
}
}
这里的 mResetWhenStop 是 Inspector 上的配置。
这说明 TweenSequence 把"停止时是否恢复原始状态"作为组件行为的一部分。
这个设计很适合 UI。
比如一个窗口打开时播放弹出动画,关闭或停止时希望回到原始 Transform。
如果每次都让调用方自己记原始坐标,就会很重复。
TweenSequence 直接把这件事放进组件里处理。
八、循环限制不同:TweenSequence 禁止 SELF 模式循环
DOTween 的循环能力很强。
常见写法是:
bash
tween.SetLoops(-1);
或者:
bash
sequence.SetLoops(-1);
MyFramework 的 TweenSequence 也支持循环:
bash
public bool mLoop;
但是它有一个限制:
存在 SELF 模式轨道时不允许循环播放。
真实代码:
bash
public void play()
{
if (mLoop && hasSelfValueType())
{
logError("存在SELF模式轨道时不允许循环播放");
mLoop = false;
}
...
}
TweenGroup 里会检查是否存在 SELF:
bash
public bool hasSelfValueType()
{
foreach (TweenTrack track in mTrackList)
{
if (track.mStartMode == START_MODE.SELF || track.mTargetMode == TARGET_MODE.SELF)
{
return true;
}
}
return false;
}
Editor 里也会禁止勾选 Loop:
bash
EditorGUI.BeginDisabledGroup(mSequence.hasSelfValueType());
toggle("Loop", ref mSequence.mLoop);
EditorGUI.EndDisabledGroup();
if (mSequence.hasSelfValueType())
{
mSequence.mLoop = false;
EditorGUILayout.HelpBox("存在SELF模式轨道时不允许循环播放", MessageType.Warning);
}
这个限制很合理。
因为 SELF 模式会在播放时取当前对象状态。
如果循环播放,每一轮的起点或目标可能不断基于上一次结果变化,容易出现位置、缩放、旋转持续偏移。
所以 TweenSequence 直接在规则层限制掉。
这也是框架内工具和通用工具的区别。
DOTween 给你更大的自由度。
TweenSequence 会加更多项目规则,避免常见错误。
九、适合场景不同
DOTween 更适合这些场景:
- 代码里动态创建动画
- 需要 Tween 任意属性
- 需要丰富 Ease 类型
- 需要 Path、Punch、Shake 等复杂 Tween
- 需要完整 Tween 生态
- 需要快速写临时动画
- 需要和第三方工具或插件配合
- 项目没有自己的动画组件体系
MyFramework 的 TweenSequence 更适合这些场景:
- 动画挂在 Prefab 上配置
- 希望在 Inspector 里编辑和预览
- 动画主要是位置、缩放、旋转
- UI 打开、关闭、提示、弹出这类固定动画
- 需要接入
Transformable组件体系 - 需要统一
setPosition / setScale / setRotation - 需要停止时恢复初始状态
- 需要限制 SELF 模式循环这类项目规则
- 不希望业务层直接管理 Tween 句柄
所以这两个东西不是同一个目标。
DOTween 更适合通用 Tween 编程。
TweenSequence 更适合 MyFramework 内部的可配置动画序列。
总结
MyFramework 的 TweenSequence 和 DOTween 最大的区别,不是能不能移动、缩放、旋转。
而是定位不同。
DOTween 是成熟的通用 Tween 引擎。
它提供丰富 API,适合代码动态创建各种动画。
MyFramework 的 TweenSequence 是框架内部的动画序列组件。
它通过:
TweenSequenceTweenGroupTweenTrackCOMTransformableSequenceCmdTransformableSequence
把一段位置、缩放、旋转动画接入自己的框架生命周期。
从代码上可以看到:
TweenSequence负责保存 Group 和 TrackTweenGroup负责组织一组轨道TweenTrack负责单条轨道的类型、曲线、起点、终点、延迟和持续时间COMTransformableSequence负责更新时间并把结果设置到TransformableCmdTransformableSequence负责对外提供统一播放和停止入口TweenSequenceAuthoringEditor负责 Inspector 配置和编辑器预览
所以它不是为了替代 DOTween。
它解决的是另一个问题:
如何让动画序列成为 MyFramework 自己框架体系的一部分。
一句话总结:
DOTween 更像一个强大的通用动画引擎。
TweenSequence 更像 MyFramework 内部的可配置动画组件。
通用引擎适合解决各种 Tween 需求。
框架内组件适合解决项目里固定、可配置、可预览、可接入生命周期的动画流程。