MyFramework Unity:TweenSequence 和 DOTween 有什么区别

📅 2026/7/6 2:26:18
MyFramework Unity:TweenSequence 和 DOTween 有什么区别
Unity 项目里做 Tween 动画很多人第一时间会想到 DOTween。这很正常。DOTween 是非常成熟的 Unity Tween 引擎常见写法很简单transform.DOMove(targetPos, 0.3f); transform.DOScale(Vector3.one, 0.3f); transform.DORotate(targetRot, 0.3f);如果要做组合动画可以使用SequenceSequence 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是一个MonoBehaviourpublic class TweenSequence : MonoBehaviour { public ListTweenGroup 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 / JoinTweenSequence 用 Group / TrackDOTween 组织 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这种链式代码组织而是用TweenSequenceTweenGroupTweenTrackTweenSequence里有多个TweenGrouppublic ListTweenGroup mGroupList new();每个TweenGroup里有多个TweenTrack[Serializable] public class TweenGroup { public ListTweenTrack 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 对象和属性。常见的有MoveRotateScaleColorFadeTextMaterialPathValue TweenCallbackSequenceLoopEase它适合做各种类型的 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 接入 COMTransformableSequenceDOTween 播放时一般会返回 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.tryGetUnityComponentTweenSequence()); // 需要启用组件更新时,则开启组件拥有者的更新,后续也不会再关闭 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 里还会处理ScaleAnchorprotected 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; }编辑器里可以添加 Groupif (button(Add Group)) { Undo.RecordObject(mSequence, Add Group); mSequence.mGroupList.Add(new TweenGroup()); EditorUtility.SetDirty(mSequence); }可以添加 Trackif (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里会检查是否存在 SELFpublic bool hasSelfValueType() { foreach (TweenTrack track in mTrackList) { if (track.mStartMode START_MODE.SELF || track.mTargetMode TARGET_MODE.SELF) { return true; } } return false; }Editor 里也会禁止勾选 LoopEditorGUI.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 需求。框架内组件适合解决项目里固定、可配置、可预览、可接入生命周期的动画流程。