DOTween:用 Unity 原生协程替换 DOTween 插件
DOTween:用 Unity 原生协程替换 DOTween 插件
本文目的:将博客园文章做本地知识库缓存,便于后续跨项目复用与检索。
原文来源:【Unity】 原生Unity写法替换Dotween插件(posted @ 2026-01-16)
说明:以下“原文备份”部分尽量保持与原文一致,用于离线查阅;如需二次落地到项目,请以项目实际代码为准。⚠️ 本地文档已更新,尚未同步到博客园;上次同步:2026-02-02(如需同步,走知识库的博客园同步流程)。
快速入口(30 秒上手)
你未来只要对我说一句:“帮我替换 DOTween(basic_only)”,我会按本文 SOP 执行:先扫描 → 输出替换清单 → 你确认 → 再替换 → 最后验收与清理。
- 本次“可自动替换”覆盖范围(basic_only):
DOVirtual.DelayedCallDOMove / DOLocalMove / DOLocalMoveYDOScaleDORotate / DOLocalRotateDOFade / DOColorSetEase(映射到EaseType或AnimationCurve)OnComplete(回调)DOTween.Init / DOTween.Clear(如项目里存在)
- 明确不在自动替换范围(命中后会单独列“需人工确认/架构决策”):
Sequence、Tweener 句柄生命周期、Kill/Pause/Restart/SetUpdate/SetAutoKill等。
0. AI 辅助替换流程(SOP:先清单后替换)
目标:把“替换 DOTween”这件事变成可复盘、可批量、可回滚的流程,而不是在项目里手工零散改动。
0.0 一句口令(给 AI 的任务描述模板)
把下面这段直接发给我即可(你只需要填项目路径,其他我会自己扫描):
请对 Unity 项目执行 DOTween 替换(basic_only),流程必须为:先扫描 -> 输出替换清单 -> 等我确认 -> 再开始替换 -> 替换后全项目检索验收 -> 清理 DOTween 残留(如存在)。
项目路径:<你的Unity项目根目录>
扫描范围:Assets/
关键字:using DG.Tweening, DG.Tweening, DOTween, DOVirtual, DOTweenAnimation, DOTweenPath, .DO*(
替换目标:协程/Unity 原生(可使用本文的 UnityDoTween 思路)
要求:最小侵入、注释零丢失;超出 basic_only 的命中点必须标记为“需人工确认”,不得自动硬替换。
0.1 输入给 AI 的信息模板(建议直接复制)
- 项目路径:
<你的Unity项目根目录> - 需要替换的范围:
Assets/(优先),必要时扩展到Packages/(谨慎) - 关键字扫描(至少包含):
using DG.TweeningDG.TweeningDOTweenDOVirtualDOTweenAnimationDOTweenPath.DO*((例如.DOMove(、.DOScale()
- 替换目标:使用 Unity 原生协程(或自封装 Tween 层)替代 DOTween
- 门禁:先输出“替换清单”并等待确认,再开始任何替换
0.2 AI 输出物要求(替换清单格式)
对每个命中点输出一行(表格/列表均可),至少包含:
- 文件路径
- 命中片段(原始代码/序列化片段)
- 命中类型:脚本调用 / Prefab&Scene 组件 / 插件目录残留
- 建议替代方案(对应到本文“对照表/封装函数”)
- 风险等级:
- 低:一次性动画,无 Tweener/Sequence 句柄依赖
- 中:有回调链/多段动画链,但可用协程串起来
- 高:依赖
Sequence、Tweener 句柄生命周期(Kill/Pause/Restart/SetUpdate/SetAutoKill等)
- 需要人工确认点:例如“是否需要可取消/可暂停/可复用的动画驱动模型”
0.3 替换执行(建议分批次)
-
前置:确保替代实现已落地到项目(必须项)
- 若项目中不存在用于替代的
UnityDoTween.cs(或你自有的等价 Tween 封装),则先执行:- 路径优先级:
- 优先按原文核心代码标注路径:
Assets/MyAssets/Scripts/UnityDoTween.cs - 若你的项目目录结构不同(例如没有
Assets/MyAssets/Scripts/),则在开始替换前先确认脚本放置位置(你指定一个项目内路径即可,例如Assets/Scripts/UnityDoTween.cs或你项目约定的工具目录)。
- 优先按原文核心代码标注路径:
- 将本文 “4.1 核心实现文件:UnityDoTween.cs(原文备份)” 的代码块 原样复制 到该脚本(保持命名空间/类名一致;若你要求改命名空间,需要同步调整所有引用)
- 路径优先级:
- 说明:这样后续替换时才能把 DOTween 调用稳定映射到
UnityDoTween.*,否则会出现“替换完成但项目缺少目标实现”的编译错误。
- 若项目中不存在用于替代的
-
批次 1(低风险):一次性移动/缩放/淡入淡出等,直接改为协程封装
-
批次 2(中风险):多段链式动画,改为“协程串联 + 回调”,并补充对象销毁时的停止策略
-
批次 3(高风险):先做架构锚定(动画驱动模型/取消机制/Runner 归属),否则容易形成“补丁堆叠”
0.4 验收与清理(必须项)
- 全项目检索:在
Assets/范围内确保 0 命中:DG.Tweening/DOTween/DOVirtual/DOTweenAnimation/DOTweenPath
- Prefab/Scene 组件检查:若存在 DOTween 相关组件(例如
DOTweenAnimation),必须替换为脚本/动画机/协程方案后再清理插件 - 最终清理:确认 0 引用后,删除 DOTween 插件目录及其
.meta(若项目中存在)
1. 现象/复现
- 项目中大量使用
DOTween(如DOMove/DOScale/DOVirtual.DelayedCall等)。 - 需求/动机:减少第三方插件依赖、降低包体大小、提高项目可维护性(原文口径)。
2. 影响
- 依赖
DOTween作为外部插件:升级/兼容/裁剪时有额外成本。 - 若目标平台或合规要求不希望引入额外第三方:需要替代方案。
3. 根因
- 动画缓动能力依赖
DOTween的 API(链式调用 + Ease + OnComplete + DelayedCall)。 - 替代思路:用 Unity 原生
Coroutine+Lerp/AnimationCurve封装一层“兼容 API”。
4. 解决方案(原文方案:UnityDoTween.cs + 替换调用点)
4.1 核心实现文件:UnityDoTween.cs(原文备份)
原文标注核心实现文件:
Assets/MyAssets/Scripts/UnityDoTween.cs
注意:以下代码段来自原文缓存(保持原样)。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace MyAssets.Scripts {
public class UnityDoTween : MonoBehaviour {
// 存储所有运行中的协程,用于清理
private static List<Coroutine> activeCoroutines = new List<Coroutine>();
private static MonoBehaviour coroutineRunner;
// 初始化方法(兼容 DOTween.Init 调用)
public static void Init(bool recycleAllByDefault = false, bool useSafeMode = true) {
// Unity 原生实现不需要特殊初始化
activeCoroutines.Clear();
}
// 清理方法(兼容 DOTween.Clear 调用)
public static void Clear(bool destroy = false) {
// 停止所有记录的协程
if (coroutineRunner != null) {
foreach (var coroutine in activeCoroutines) {
if (coroutine != null) {
coroutineRunner.StopCoroutine(coroutine);
}
}
}
activeCoroutines.Clear();
}
// 获取协程运行器
private static MonoBehaviour GetCoroutineRunner() {
if (coroutineRunner == null) {
// 尝试找到一个 MonoBehaviour 来运行协程
coroutineRunner = UnityEngine.Object.FindFirstObjectByType<MonoBehaviour>();
}
return coroutineRunner;
}
// 延迟调用方法(兼容 DOVirtual.DelayedCall)
public static void DelayedCall(float delay, Action callback) {
var runner = GetCoroutineRunner();
if (runner != null) {
var coroutine = runner.StartCoroutine(DODelay(delay, callback));
activeCoroutines.Add(coroutine);
}
}
// 缓动类型枚举
public enum EaseType {
Linear,
InQuad,
OutQuad,
InOutQuad,
InCubic,
OutCubic,
InOutCubic
}
// 获取缓动值
private static float GetEaseValue(float t, EaseType easeType) {
switch (easeType) {
case EaseType.InQuad:
return t * t;
case EaseType.OutQuad:
return t * (2 - t);
case EaseType.InOutQuad:
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
case EaseType.InCubic:
return t * t * t;
case EaseType.OutCubic:
return (--t) * t * t + 1;
case EaseType.InOutCubic:
return t < 0.5f ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
case EaseType.Linear:
default:
return t;
}
}
// 移动动画
public static IEnumerator DOMove(Transform target, Vector3 endValue, float duration ,float delay = 0f, bool from = false,Action onComplete = null, EaseType easeType = EaseType.Linear) {
if(delay >0)
yield return new WaitForSeconds(delay);
Vector3 startValue = target.position;
float elapsedTime = 0f;
if (from) {
startValue = endValue;
endValue = target.position;
}
while (elapsedTime < duration) {
float t = GetEaseValue(elapsedTime / duration, easeType);
target.position = Vector3.Lerp(startValue, endValue, t);
elapsedTime += Time.deltaTime;
yield return null;
}
target.position = endValue;
onComplete?.Invoke();
}
// 本地移动动画
public static IEnumerator DOLocalMove(Transform target, Vector3 endValue, float duration ,float delay = 0f, bool from = false,Action onComplete = null, EaseType easeType = EaseType.Linear) {
if(delay >0)
yield return new WaitForSeconds(delay);
Vector3 startValue = target.localPosition;
float elapsedTime = 0f;
if (from) {
startValue = endValue;
endValue = target.localPosition;
}
while (elapsedTime < duration) {
float t = GetEaseValue(elapsedTime / duration, easeType);
target.localPosition = Vector3.Lerp(startValue, endValue, t);
elapsedTime += Time.deltaTime;
yield return null;
}
target.localPosition = endValue;
onComplete?.Invoke();
}
// 本地Y轴移动动画
public static IEnumerator DOLocalMoveY(Transform target, float endValue, float duration, float delay = 0f, Action onComplete = null, EaseType easeType = EaseType.Linear) {
if(delay > 0)
yield return new WaitForSeconds(delay);
float startValue = target.localPosition.y;
float elapsedTime = 0f;
while (elapsedTime < duration) {
float t = GetEaseValue(elapsedTime / duration, easeType);
float currentY = Mathf.Lerp(startValue, endValue, t);
target.localPosition = new Vector3(target.localPosition.x, currentY, target.localPosition.z);
elapsedTime += Time.deltaTime;
yield return null;
}
target.localPosition = new Vector3(target.localPosition.x, endValue, target.localPosition.z);
onComplete?.Invoke();
}
// 旋转动画
public static IEnumerator DORotate(Transform target, Vector3 endValue, float duration) {
Quaternion startRotation = target.rotation;
Quaternion endRotation = Quaternion.Euler(endValue);
float elapsedTime = 0f;
while (elapsedTime < duration) {
target.rotation = Quaternion.Lerp(startRotation, endRotation, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
target.rotation = endRotation;
}
public static IEnumerator DORotate(Transform target, Quaternion endValue, float duration) {
Quaternion startRotation = target.rotation;
float elapsedTime = 0f;
while (elapsedTime < duration) {
target.rotation = Quaternion.Lerp(startRotation, endValue, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
target.rotation = endValue;
}
// 本地旋转动画
public static IEnumerator DOLocalRotate(Transform target, Vector3 endValue, float duration) {
Quaternion startRotation = target.localRotation;
Quaternion endRotation = Quaternion.Euler(endValue);
float elapsedTime = 0f;
while (elapsedTime < duration) {
target.localRotation = Quaternion.Lerp(startRotation, endRotation, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
target.localRotation = endRotation;
}
// 缩放动画
public static IEnumerator DOScale(Transform target, Vector3 endValue, float duration,float delay = 0f, bool from = false,Action onComplete = null, EaseType easeType = EaseType.Linear) {
if(delay >0)
yield return new WaitForSeconds(delay);
Vector3 startValue = target.localScale;
float elapsedTime = 0f;
if (from) {
startValue = endValue;
endValue = target.localScale;
}
while (elapsedTime < duration) {
float t = GetEaseValue(elapsedTime / duration, easeType);
target.localScale = Vector3.Lerp(startValue, endValue, t);
elapsedTime += Time.deltaTime;
yield return null;
}
target.localScale = endValue;
onComplete?.Invoke();
}
// UI透明度动画
public static IEnumerator DOFade(CanvasGroup canvasGroup, float endValue, float duration, float delay = 0f, bool from = false,Action onComplete = null) {
if(delay >0)
yield return new WaitForSeconds(delay);
float startValue = canvasGroup.alpha;
float elapsedTime = 0f;
if (from) {
startValue = endValue;
endValue = canvasGroup.alpha;
}
while (elapsedTime < duration) {
canvasGroup.alpha = Mathf.Lerp(startValue, endValue, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
canvasGroup.alpha = endValue;
}
// Image颜色动画
public static IEnumerator DOColor(Image image, Color endValue, float duration) {
Color startValue = image.color;
float elapsedTime = 0f;
while (elapsedTime < duration) {
image.color = Color.Lerp(startValue, endValue, elapsedTime / duration);
elapsedTime += Time.deltaTime;
yield return null;
}
image.color = endValue;
}
// 数值动画
public static IEnumerator DOFloat(System.Action<float> onValueChanged, float startValue, float endValue,
float duration) {
float elapsedTime = 0f;
while (elapsedTime < duration) {
float currentValue = Mathf.Lerp(startValue, endValue, elapsedTime / duration);
onValueChanged?.Invoke(currentValue);
elapsedTime += Time.deltaTime;
yield return null;
}
onValueChanged?.Invoke(endValue);
}
// 带缓动函数的移动
public static IEnumerator DOMoveEase(Transform target, Vector3 endValue, float duration,
AnimationCurve easeCurve = null) {
Vector3 startValue = target.position;
float elapsedTime = 0f;
if (easeCurve == null) {
easeCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
}
while (elapsedTime < duration) {
float t = easeCurve.Evaluate(elapsedTime / duration);
target.position = Vector3.Lerp(startValue, endValue, t);
elapsedTime += Time.deltaTime;
yield return null;
}
target.position = endValue;
}
public static IEnumerator DODelay(float delay, Action onComplete = null) {
yield return new WaitForSeconds(delay);
onComplete?.Invoke();
}
}
}
4.2 原文:功能清单(缓存)
- 静态方法(兼容 DOTween API)
Init(bool, bool):初始化(兼容调用,无实际操作)Clear(bool):清理所有运行中的动画协程DelayedCall(float, Action):延迟调用回调函数
- 动画协程方法:
DOMove / DOLocalMove / DOLocalMoveY / DOScale / DORotate / DOLocalRotate / DOFade / DOColor / DOFloat / DOMoveEase / DODelay - 缓动枚举:
EaseType(Linear/InQuad/OutQuad/InOutQuad/InCubic/OutCubic/InOutCubic)
4.3 原文:引用替换要点(缓存)
- 将
using DG.Tweening替换为(原文示例):
using MyAssets.Scripts;
4.4 原文:用法对照表(缓存)
说明:为提高可读性与 AI 可复制性,这里将原文“分小节的代码块示例”压缩为表格(示例语义保持一致)。
| DOTween 能力点 | DOTween 写法(示例) | 替换为 UnityDoTween/协程写法(示例) | 注意事项 |
|---|---|---|---|
| 基础移动 | transform.DOMove(targetPos, 0.5f).OnComplete(() => Debug.Log("完成")); |
StartCoroutine(UnityDoTween.DOMove(transform, targetPos, 0.5f, 0f, false, () => Debug.Log("完成"))); |
onComplete 用回调参数即可 |
| 带缓动移动 | transform.DOMove(targetPos, 0.5f).SetEase(Ease.InQuad).OnComplete(callback); |
StartCoroutine(UnityDoTween.DOMove(transform, targetPos, 0.5f, 0f, false, callback, UnityDoTween.EaseType.InQuad)); |
SetEase 映射到 EaseType(或改用 AnimationCurve) |
| 本地 Y 移动 | transform.DOLocalMoveY(targetY, 0.5f).SetEase(Ease.InQuad); |
StartCoroutine(UnityDoTween.DOLocalMoveY(transform, targetY, 0.5f, 0f, null, UnityDoTween.EaseType.InQuad)); |
注意这里的参数顺序与回调位置 |
| 缩放 | transform.DOScale(Vector3.zero, 0.3f).OnComplete(() => Destroy(gameObject)); |
StartCoroutine(UnityDoTween.DOScale(transform, Vector3.zero, 0.3f, 0f, false, () => Destroy(gameObject))); |
销毁/回收逻辑放 onComplete |
| 延迟调用 | DOVirtual.DelayedCall(1.5f, () => DoSomething()); |
UnityDoTween.DelayedCall(1.5f, () => DoSomething()); |
若需要取消/统一管理,需定义取消机制(不在 basic_only 自动替换范围) |
| 初始化与清理 | DOTween.Init(false, true); DOTween.Clear(true); |
UnityDoTween.Init(false, true); UnityDoTween.Clear(true); |
是否需要 Init/Clear 取决于项目是否真的依赖 DOTween 全局初始化 |
5. 如何验证(原文验证清单:缓存)
说明:原文给出的验证项包含具体业务脚本/场景文件名,属于“项目化验证”。为了跨项目复用,本知识库将其替换为通用验证清单。
- 编译/导入:Unity 重新导入(或重新打开项目)后 Console 0 error(至少无 CS 编译错误)。
- 全局检索(Assets 范围):确保 0 命中:
DG.Tweening/DOTween/DOVirtual/DOTweenAnimation/DOTweenPath。 - Prefab/Scene 检查:确认场景与 Prefab 上不存在 DOTween 组件(如
DOTweenAnimation/DOTweenPath);若存在,先替代后清理。 - 运行验证:覆盖所有动画触发路径(UI 动画、道具动画、角色动画、过场动画等),确保行为一致。
- 清理验证:删除 DOTween 插件目录后(若项目中存在),再次编译无报错。
6. 下次怎么避免(知识库补充)
- 若项目需要“可裁剪/可替换”的 Tween:优先封装一层自己的 Tween API(例如
ITweenRunner),避免业务层直接绑死某个插件。 - 新增动画能力时,优先把“生命周期/停止策略/Runner 选择策略”写成明确约定(避免后期出现难以停协程/难以回收的问题)。
本文来自博客园,作者:星空探险家,转载请注明原文链接:https://www.cnblogs.com/PuppetLazy/p/19172054

浙公网安备 33010602011771号