Unity 项目里做 Tween 动画,很多人第一时间会想到 DOTween。
这很正常。
DOTween 是非常成熟的 Unity Tween 引擎,常见写法很简单:
transform.DOMove(targetPos, 0.3f); transform.DOScale(Vector3.one, 0.3f); transform.DORotate(targetRot, 0.3f);如果要做组合动画,可以使用Sequence:
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 到底有什么区别。
项目地址:
https://github.com/ZHOURUIH/MyFramework
一、定位不同:通用 Tween 引擎 vs 框架内动画序列组件
DOTween 的定位是通用 Tween 引擎。
它面向的是整个 Unity 项目。
你可以用它处理:
- Transform 动画
- UI 动画
- 材质动画
- 颜色动画
- 数值动画
- 路径动画
- 回调
- 延迟
- 循环
- Sequence 拼接
它的优势是功能非常完整,API 也非常丰富。
典型 DOTween 写法是代码驱动:
transform.DOLocalMove(new Vector3(100.0f, 0.0f, 0.0f), 0.3f) .SetEase(Ease.OutQuad) .OnComplete(onMoveDone);如果要组合动画,就写:
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:
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 的方式是代码链式调用。
比如顺序播放:
Sequence sequence = DOTween.Sequence(); sequence.Append(transform.DOLocalMove(pos0, 0.3f)); sequence.Append(transform.DOScale(scale0, 0.2f));同时播放:
Sequence sequence = DOTween.Sequence(); sequence.Append(transform.DOLocalMove(pos0, 0.3f)); sequence.Join(transform.DOScale(scale0, 0.3f));插入到指定时间:
sequence.Insert(0.5f, transform.DORotate(rot, 0.3f));这种方式非常灵活。
代码里可以动态决定添加哪些 Tween,执行顺序也很自由。
MyFramework 的TweenSequence是另一种组织方式。
它不是用Append、Join这种链式代码组织,而是用:
TweenSequenceTweenGroupTweenTrack
TweenSequence里有多个TweenGroup:
public List<TweenGroup> mGroupList = new();每个TweenGroup里有多个TweenTrack:
[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的字段是这样的:
[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当前能力更收敛。
它主要处理三类变换:
// 缓动的类型 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 对象。
比如:
Tween tween = transform.DOLocalMove(targetPos, 0.3f); tween.Kill();或者:
Sequence sequence = DOTween.Sequence(); sequence.Append(transform.DOLocalMove(targetPos, 0.3f)); sequence.Kill();所以 DOTween 的生命周期通常由 Tween / Sequence 句柄控制。
MyFramework 的播放入口不是这样。
它通过框架命令CmdTransformableSequence播放。
真实代码:
// 缓动序列 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:
// 用于播放物体的缓动序列 public class COMTransformableSequence : GameComponent { protected SequenceCallback mDoneCallback; // 序列播放完成的回调,参数1:当前组件,参数2:是否被打断 protected TweenSequence mSequence; // 当前正在播放的缓动序列 protected float mCurrentTime; // 从上一次从头开始播放到现在的时长 protected float mTotalLength; // 序列的总长度,即所有TweenGroup中最长的长度 protected PLAY_STATE mPlayState; // 播放状态 }更新时由框架组件推进时间:
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 里常见写法是直接传目标值:
transform.DOLocalMove(targetPos, 0.3f);如果要相对移动,可以使用 DOTween 的相对模式,例如SetRelative()。
MyFramework 的TweenTrack里,起点和终点有自己的模式。
起始值模式:
// 缓动的起始值的类型 public enum START_MODE : byte { VALUE, // 编辑器配置的固定值 SELF, // 播放时取节点当前值 }目标值模式:
// 缓动的目标类型 public enum TARGET_MODE : byte { VALUE, // 固定值 TRANSFORM_REALTIME, // 指定节点,并且在移动过程中实时获取节点的值进行调整,适用于目标对象会移动的情况 TRANSFORM_SNAPSHOT, // 指定节点,但是只在开始移动时获取节点的值进行调整,适用于目标对象不会移动的情况 SELF, // 指定节点,但是只在开始移动时获取节点的值进行调整,适用于目标对象不会移动的情况 }这里很有框架味道。
它把常见动画需求分成了几类:
第一,固定值。
比如从配置的 A 点移动到配置的 B 点。
第二,播放时取自己当前值。
比如 UI 当前在哪里,就从哪里开始弹出。
第三,目标节点快照。
比如播放开始时取某个目标节点的位置,之后不再变化。
第四,目标节点实时值。
比如目标对象会移动,动画过程中持续追踪目标位置。
TweenTrack.getTargetValue就是按这个规则取目标值:
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; }播放开始时会缓存运行时起点和目标点:
// 开始播放 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:
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:
[CustomEditor(typeof(TweenSequence))] [CanEditMultipleObjects] public class TweenSequenceAuthoringEditor : GameInspector { private TweenSequence mSequence; private bool mPlaying; private float mPreviewTime; private double mStartTime; }编辑器里可以添加 Group:
if (button("Add Group")) { Undo.RecordObject(mSequence, "Add Group"); mSequence.mGroupList.Add(new TweenGroup()); EditorUtility.SetDirty(mSequence); }可以添加 Track:
if (button("Add Track")) { Undo.RecordObject(mSequence, "Add Track"); group.mTrackList.Add(new TweenTrack()); }可以选择类型、曲线、持续时间、延迟、起始值、目标值。
比如这里显示 Track 的核心配置:
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);它还支持在编辑器里预览。
点击播放时:
private void StartPlay() { mPlaying = true; // 播放之前先确认所有轨道都是在停止状态的 mSequence.stop(true); mSequence.play(); mStartTime = EditorApplication.timeSinceStartup; EditorApplication.update -= UpdatePreview; EditorApplication.update += UpdatePreview; }预览更新时会直接调用:
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 句柄调用:
tween.Kill(); sequence.Kill();是否回到初始状态,需要自己处理,或者使用其他方式组合。
MyFramework 的TweenSequence内部会记录播放前的位置、缩放、旋转。
播放时记录:
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; }停止时可以还原:
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 的循环能力很强。
常见写法是:
tween.SetLoops(-1);或者:
sequence.SetLoops(-1);MyFramework 的 TweenSequence 也支持循环:
public bool mLoop;但是它有一个限制:
存在 SELF 模式轨道时不允许循环播放。
真实代码:
public void play() { if (mLoop && hasSelfValueType()) { logError("存在SELF模式轨道时不允许循环播放"); mLoop = false; } ... }TweenGroup里会检查是否存在 SELF:
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:
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 需求。
框架内组件适合解决项目里固定、可配置、可预览、可接入生命周期的动画流程。