DOTween:用 Unity 原生协程替换 DOTween 插件

DOTween:用 Unity 原生协程替换 DOTween 插件

本文目的:将博客园文章做本地知识库缓存,便于后续跨项目复用与检索。
原文来源:【Unity】 原生Unity写法替换Dotween插件(posted @ 2026-01-16)
说明:以下“原文备份”部分尽量保持与原文一致,用于离线查阅;如需二次落地到项目,请以项目实际代码为准。

⚠️ 本地文档已更新,尚未同步到博客园;上次同步:2026-02-02(如需同步,走知识库的博客园同步流程)。


快速入口(30 秒上手)

你未来只要对我说一句:“帮我替换 DOTween(basic_only)”,我会按本文 SOP 执行:先扫描 → 输出替换清单 → 你确认 → 再替换 → 最后验收与清理

  • 本次“可自动替换”覆盖范围(basic_only)
    • DOVirtual.DelayedCall
    • DOMove / DOLocalMove / DOLocalMoveY
    • DOScale
    • DORotate / DOLocalRotate
    • DOFade / DOColor
    • SetEase(映射到 EaseTypeAnimationCurve
    • 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.Tweening
    • DG.Tweening
    • DOTween
    • DOVirtual
    • DOTweenAnimation
    • DOTweenPath
    • .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 选择策略”写成明确约定(避免后期出现难以停协程/难以回收的问题)。
posted @ 2026-01-16 13:43  星空探险家  阅读(20)  评论(0)    收藏  举报