【Unity编辑器扩展】解决uGUI动效痛点 零代码可视化快速制作UI动效 DOTween Sequence可视化

UI动效痛点:

UI动效一直是Unity游戏开发的一大痛点,大部分项目都在使用Animator/Animation制作UI动效。而Animation是以节点路径记录动画,一旦UI层级、节点名变更就会导致动效返工,且Animation编辑器缓动曲线很难控制,没有预置常用曲线可直接用。易用性、可维护性苦不堪言。

一小部分团队则是使用DOTween让程序员手写动效,即使DOTween简单易用,但脱离动效师主导很难制作出高品质的动效,且使用代码制作动效更加难以维护。

如果把DOTween强大的动效功能,以可视化、零代码的方式使用,即使对于动效师也远比使用Animation简单快速,同时又能解决使用Animation的痛点。

实际上DOTween Pro提供了一个可视化脚本DOTween Animation,坏消息是,不支持Sequence,很不好用,且代码也没有优化gc。

效果预览:

以下是作者花了两三分钟随便点点制作出的UI动效,不代表DOTween的功能上限,仅供参考。

无需运行,编辑器下随时预览动效,所见即所得。

工具需求:

工具的最终目的是简化UI动效制作、降低UI动效的制作门槛,并且易于复用以及维护。

UI动画通常是由一连串的多个组件的动效衔接而成,因此DOTween Pro自带的DOTweenAnimation(单个动效)无法满足需求。最适合的还是用Sequence,支持多个动效并行、串联播放,以实现复杂的动效需求。

并且每个动效都需要支持生命周期回调,比如:某个动效播放完成后需要执行一个函数功能等。

当然,写工具之前首先要了解市面上是否存在优秀的替代品,遗憾的是AssetStore竟然没有找到一款需求相近的插件。

不过github有个与设想高度重合的开源UI动效插件:Unity-AnimationUI

坏消息是此插件自己实现了一套缓动,经过试用发现还存在报错和倒放时回调触发时机错误的bug,最大的问题还是没有依赖比较成熟且免费的DOTween。

因此,最佳方案就是实现个基于DOTween Sequence的可视化脚本。

工具设计:

由于DOTween接口较多,不同组件扩展了多个接口且参数不一,因此需要用最小的内存代价适配所有参数设置;虽然DOTween参数较多,但是不能在面板上显示太多的参数而导致凌乱,可通过勾选框切换不同的参数模式。

  1. Add Type: 也就是DOTween.Sequence添加动画的方式,Join并行或Append串行(不懂的去学习DOTween,不再赘述);

  2. Target:需要动的物体;

  3. AnimationType: 动效类型枚举,控制Target的动效方式,如DOMove、DOScale等等;

  4. Delay:动画播放延迟时间;

  5. From (勾选Enable启用):播放动画前会把Target设置到该值,作为初始值。(非常实用)

  6. To :目标值,可填具体值,勾选ToTarget可将某个组件的值作为目标值,例如把某节点的位置作为移动目标点,而不是一个静态的数值。(非常实用)

  7. DurationOrSpeed:动效的过渡用时或速度。勾选UseSpeed该值会作为速度使用(例如位移动效,填10则为每秒10米)

  8. Ease:缓动类型,DOTween内置了三十多个缓动类型。勾选Use Curve即可自定义缓动曲线;

  1. Loops: 动效循环播放次数,-1为循环播放。支持设置循环类型;

  2. UpdateType: 动效刷新类型,用于设置动效在Update、FixedUpdate、LateUpdate中计算;

  3. Animation Events: 动效的回调方法;

代码实现:

完整代码可以从开源自动化游戏框架GF_X中获取:https://github.com/sunsvip/GF_X/blob/master/Assets/AAAGame/Scripts/Common/DOTweenSequence.cshttps://github.com/sunsvip/GF_X/blob/master/Assets/AAAGame/Scripts/Common/DOTweenSequence.cs

主要是编辑器扩展功能,没有任何值得一提的难点,直接附上编辑器代码和Runtime代码:

cs 复制代码
using DG.Tweening;
using System;
using UnityEngine;
using static DOTweenSequence;
using UnityEngine.Events;
using UnityEngine.UI;

#if UNITY_EDITOR
using DG.DOTweenEditor;
using UnityEditorInternal;
using UnityEditor;

#region Editor Inspector
[CanEditMultipleObjects]
[CustomEditor(typeof(DOTweenSequence))]
public class DOTweeSequenceInspector : Editor
{
    SerializedProperty m_Sequence;
    ReorderableList m_SequenceList;
    private void OnEnable()
    {
        m_Sequence = serializedObject.FindProperty("m_Sequence");
        m_SequenceList = new ReorderableList(serializedObject, m_Sequence);
        m_SequenceList.drawElementCallback = OnDrawSequenceItem;
        m_SequenceList.elementHeightCallback = index =>
        {
            var item = m_Sequence.GetArrayElementAtIndex(index);
            return EditorGUI.GetPropertyHeight(item);
        };
        m_SequenceList.drawHeaderCallback = OnDrawSequenceHeader;
    }

    public override void OnInspectorGUI()
    {
        if (!EditorApplication.isPlaying)
        {
            EditorGUILayout.BeginHorizontal();
            {
                if (GUILayout.Button("Play"))
                {
                    if (DOTweenEditorPreview.isPreviewing)
                    {
                        DOTweenEditorPreview.Stop(true, true);
                        (target as DOTweenSequence).DOKill();
                    }
                    DOTweenEditorPreview.PrepareTweenForPreview((target as DOTweenSequence).DOPlay());
                    DOTweenEditorPreview.Start();
                }
                if (GUILayout.Button("Rewind"))
                {
                    if (DOTweenEditorPreview.isPreviewing)
                    {
                        DOTweenEditorPreview.Stop(true, true);
                        (target as DOTweenSequence).DOKill();
                    }
                    DOTweenEditorPreview.PrepareTweenForPreview((target as DOTweenSequence).DORewind());
                    DOTweenEditorPreview.Start();
                }
                if (GUILayout.Button("Reset"))
                {
                    DOTweenEditorPreview.Stop(true, true);
                    (target as DOTweenSequence).DOKill();
                }
                EditorGUILayout.EndHorizontal();
            }
        }

        serializedObject.Update();
        m_SequenceList.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
        base.OnInspectorGUI();
    }

    private void OnDrawSequenceHeader(Rect rect)
    {
        EditorGUI.LabelField(rect, "Animation Sequences");
    }
    private void OnDrawSequenceItem(Rect rect, int index, bool isActive, bool isFocused)
    {
        SerializedProperty element = m_Sequence.GetArrayElementAtIndex(index);
        EditorGUI.PropertyField(rect, element, true);
    }
}

[CustomPropertyDrawer(typeof(SequenceAnimation))]
public class SequenceTweenMoveDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var onPlay = property.FindPropertyRelative("OnPlay");
        var onUpdate = property.FindPropertyRelative("OnUpdate");
        var onComplete = property.FindPropertyRelative("OnComplete");
        return EditorGUIUtility.singleLineHeight * 11 + (property.isExpanded ? (EditorGUI.GetPropertyHeight(onPlay) + EditorGUI.GetPropertyHeight(onUpdate) + EditorGUI.GetPropertyHeight(onComplete)) : 0);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.indentLevel++;
        var target = property.FindPropertyRelative("Target");
        var addType = property.FindPropertyRelative("AddType");
        var tweenType = property.FindPropertyRelative("AnimationType");
        var toValue = property.FindPropertyRelative("ToValue");
        var useToTarget = property.FindPropertyRelative("UseToTarget");
        var toTarget = property.FindPropertyRelative("ToTarget");
        var useFromValue = property.FindPropertyRelative("UseFromValue");
        var fromValue = property.FindPropertyRelative("FromValue");
        var duration = property.FindPropertyRelative("DurationOrSpeed");
        var speedBased = property.FindPropertyRelative("SpeedBased");
        var delay = property.FindPropertyRelative("Delay");
        var customEase = property.FindPropertyRelative("CustomEase");
        var ease = property.FindPropertyRelative("Ease");
        var easeCurve = property.FindPropertyRelative("EaseCurve");
        var loops = property.FindPropertyRelative("Loops");
        var loopType = property.FindPropertyRelative("LoopType");
        var updateType = property.FindPropertyRelative("UpdateType");
        var snapping = property.FindPropertyRelative("Snapping");
        var onPlay = property.FindPropertyRelative("OnPlay");
        var onUpdate = property.FindPropertyRelative("OnUpdate");
        var onComplete = property.FindPropertyRelative("OnComplete");

        var lastRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
        EditorGUI.PropertyField(lastRect, addType);

        EditorGUI.BeginChangeCheck();
        lastRect.y += EditorGUIUtility.singleLineHeight;
        EditorGUI.PropertyField(lastRect, target);
        lastRect.y += EditorGUIUtility.singleLineHeight;
        EditorGUI.PropertyField(lastRect, tweenType);

        if (EditorGUI.EndChangeCheck())
        {
            var fixedComType = GetFixedComponentType(target.objectReferenceValue as Component, (DOTweenType)tweenType.enumValueIndex);
            if (fixedComType != null)
            {
                target.objectReferenceValue = fixedComType;
            }
        }
        if (target.objectReferenceValue != null && null == GetFixedComponentType(target.objectReferenceValue as Component, (DOTweenType)tweenType.enumValueIndex))
        {
            lastRect.y += EditorGUIUtility.singleLineHeight;
            EditorGUI.HelpBox(lastRect, string.Format("{0}不支持{1}", target.objectReferenceValue == null ? "Target" : target.objectReferenceValue.GetType().Name, tweenType.enumDisplayNames[tweenType.enumValueIndex]), MessageType.Error);
        }

        //Delay, Snapping
        lastRect.y += EditorGUIUtility.singleLineHeight;
        var horizontalRect = lastRect;
        horizontalRect.width -= 115;
        EditorGUI.PropertyField(horizontalRect, delay);
        horizontalRect.x += horizontalRect.width;
        horizontalRect.width = 115;
        snapping.boolValue = EditorGUI.ToggleLeft(horizontalRect, "Snapping", snapping.boolValue);

        //From Value
        lastRect.y += EditorGUIUtility.singleLineHeight;
        horizontalRect = lastRect;
        horizontalRect.width -= 115;



        //ToTarget
        lastRect.y += EditorGUIUtility.singleLineHeight;
        var toRect = lastRect;
        toRect.width -= 115;

        //To Value
        var dotweenTp = (DOTweenType)tweenType.enumValueIndex;
        switch (dotweenTp)
        {
            case DOTweenType.DOMoveX:
            case DOTweenType.DOMoveY:
            case DOTweenType.DOMoveZ:
            case DOTweenType.DOLocalMoveX:
            case DOTweenType.DOLocalMoveY:
            case DOTweenType.DOLocalMoveZ:
            case DOTweenType.DOAnchorPosX:
            case DOTweenType.DOAnchorPosY:
            case DOTweenType.DOAnchorPosZ:
            case DOTweenType.DOFade:
            case DOTweenType.DOCanvasGroupFade:
            case DOTweenType.DOFillAmount:
            case DOTweenType.DOValue:
            case DOTweenType.DOScaleX:
            case DOTweenType.DOScaleY:
            case DOTweenType.DOScaleZ:
                {
                    EditorGUI.BeginDisabledGroup(!useFromValue.boolValue);
                    var value = fromValue.vector4Value;
                    value.x = EditorGUI.FloatField(horizontalRect, "From", value.x);
                    fromValue.vector4Value = value;
                    EditorGUI.EndDisabledGroup();

                    if (!useToTarget.boolValue)
                    {
                        value = toValue.vector4Value;
                        value.x = EditorGUI.FloatField(toRect, "To", value.x);
                        toValue.vector4Value = value;
                    }
                }
                break;
            case DOTweenType.DOAnchorPos:
            case DOTweenType.DOFlexibleSize:
            case DOTweenType.DOMinSize:
            case DOTweenType.DOPreferredSize:
            case DOTweenType.DOSizeDelta:
                {
                    EditorGUI.BeginDisabledGroup(!useFromValue.boolValue);
                    fromValue.vector4Value = EditorGUI.Vector2Field(horizontalRect, "From", fromValue.vector4Value);
                    EditorGUI.EndDisabledGroup();
                    if (!useToTarget.boolValue)
                        toValue.vector4Value = EditorGUI.Vector2Field(toRect, "To", toValue.vector4Value);
                }
                break;
            case DOTweenType.DOMove:
            case DOTweenType.DOLocalMove:
            case DOTweenType.DOAnchorPos3D:
            case DOTweenType.DOScale:
            case DOTweenType.DORotate:
            case DOTweenType.DOLocalRotate:
                {
                    EditorGUI.BeginDisabledGroup(!useFromValue.boolValue);
                    fromValue.vector4Value = EditorGUI.Vector3Field(horizontalRect, "From", fromValue.vector4Value);
                    EditorGUI.EndDisabledGroup();
                    if (!useToTarget.boolValue)
                        toValue.vector4Value = EditorGUI.Vector3Field(toRect, "To", toValue.vector4Value);
                }
                break;
            case DOTweenType.DOColor:
                {
                    EditorGUI.BeginDisabledGroup(!useFromValue.boolValue);
                    fromValue.vector4Value = EditorGUI.ColorField(horizontalRect, "From", fromValue.vector4Value);
                    EditorGUI.EndDisabledGroup();
                    if (!useToTarget.boolValue)
                        toValue.vector4Value = EditorGUI.ColorField(toRect, "To", toValue.vector4Value);
                }
                break;
        }
        if (useToTarget.boolValue)
        {
            toTarget.objectReferenceValue = EditorGUI.ObjectField(toRect, "To", toTarget.objectReferenceValue, target.objectReferenceValue != null ? target.objectReferenceValue.GetType() : typeof(Component), true);

            if (toTarget.objectReferenceValue == null)
            {
                lastRect.y += EditorGUIUtility.singleLineHeight;
                EditorGUI.HelpBox(lastRect, "To target cannot be null.", MessageType.Error);
            }
        }

        horizontalRect.x += horizontalRect.width;
        horizontalRect.width = 115;
        useFromValue.boolValue = EditorGUI.ToggleLeft(horizontalRect, "Enable", useFromValue.boolValue);

        toRect.x += toRect.width;
        toRect.width = 115;
        useToTarget.boolValue = EditorGUI.ToggleLeft(toRect, "ToTarget", useToTarget.boolValue);

        //Duration
        lastRect.y += EditorGUIUtility.singleLineHeight;
        horizontalRect = lastRect;
        horizontalRect.width -= 115;
        EditorGUI.PropertyField(horizontalRect, duration);
        horizontalRect.x += horizontalRect.width;
        horizontalRect.width = 115;
        speedBased.boolValue = EditorGUI.ToggleLeft(horizontalRect, "Use Speed", speedBased.boolValue);

        //Ease
        lastRect.y += EditorGUIUtility.singleLineHeight;
        horizontalRect = lastRect;
        horizontalRect.width -= 115;
        if (customEase.boolValue)
            EditorGUI.PropertyField(horizontalRect, easeCurve);
        else
            EditorGUI.PropertyField(horizontalRect, ease);
        horizontalRect.x += horizontalRect.width;
        horizontalRect.width = 115;
        customEase.boolValue = EditorGUI.ToggleLeft(horizontalRect, "Use Curve", customEase.boolValue);

        //Loops
        lastRect.y += EditorGUIUtility.singleLineHeight;
        horizontalRect = lastRect;
        horizontalRect.width -= 115;
        EditorGUI.PropertyField(horizontalRect, loops);
        horizontalRect.x += horizontalRect.width;
        horizontalRect.width = 115;
        EditorGUI.BeginDisabledGroup(loops.intValue == 1);
        loopType.enumValueIndex = (int)(LoopType)EditorGUI.EnumPopup(horizontalRect, (LoopType)loopType.enumValueIndex);
        EditorGUI.EndDisabledGroup();
        //UpdateType
        lastRect.y += EditorGUIUtility.singleLineHeight;
        EditorGUI.PropertyField(lastRect, updateType);

        //Events
        lastRect.y += EditorGUIUtility.singleLineHeight;
        property.isExpanded = EditorGUI.Foldout(lastRect, property.isExpanded, "Animation Events");
        if (property.isExpanded)
        {
            //OnPlay
            lastRect.y += EditorGUIUtility.singleLineHeight;
            EditorGUI.PropertyField(lastRect, onPlay);

            //OnUpdate
            lastRect.y += EditorGUI.GetPropertyHeight(onPlay);
            EditorGUI.PropertyField(lastRect, onUpdate);

            //OnComplete
            lastRect.y += EditorGUI.GetPropertyHeight(onUpdate);
            EditorGUI.PropertyField(lastRect, onComplete);
        }

        EditorGUI.indentLevel--;
        EditorGUI.EndProperty();
    }

    private static Component GetFixedComponentType(Component com, DOTweenType tweenType)
    {
        if (com == null) return null;
        switch (tweenType)
        {
            case DOTweenType.DOMove:
            case DOTweenType.DOMoveX:
            case DOTweenType.DOMoveY:
            case DOTweenType.DOMoveZ:
            case DOTweenType.DOLocalMove:
            case DOTweenType.DOLocalMoveX:
            case DOTweenType.DOLocalMoveY:
            case DOTweenType.DOLocalMoveZ:
            case DOTweenType.DOScale:
            case DOTweenType.DOScaleX:
            case DOTweenType.DOScaleY:
            case DOTweenType.DOScaleZ:
                return com.gameObject.GetComponent<Transform>();
            case DOTweenType.DOAnchorPos:
            case DOTweenType.DOAnchorPosX:
            case DOTweenType.DOAnchorPosY:
            case DOTweenType.DOAnchorPosZ:
            case DOTweenType.DOAnchorPos3D:
            case DOTweenType.DOSizeDelta:
                return com.gameObject.GetComponent<RectTransform>();
            case DOTweenType.DOColor:
            case DOTweenType.DOFade:
                return com.gameObject.GetComponent<UnityEngine.UI.Graphic>();
            case DOTweenType.DOCanvasGroupFade:
                return com.gameObject.GetComponent<UnityEngine.CanvasGroup>();
            case DOTweenType.DOFillAmount:
                return com.gameObject.GetComponent<UnityEngine.UI.Image>();
            case DOTweenType.DOFlexibleSize:
            case DOTweenType.DOMinSize:
            case DOTweenType.DOPreferredSize:
                return com.gameObject.GetComponent<UnityEngine.UI.LayoutElement>();
            case DOTweenType.DOValue:
                return com.gameObject.GetComponent<UnityEngine.UI.Slider>();

        }
        return null;
    }
}
#endregion
#endif
public class DOTweenSequence : MonoBehaviour
{
    [HideInInspector][SerializeField] SequenceAnimation[] m_Sequence;
    [SerializeField] bool m_PlayOnAwake = false;
    [SerializeField] float m_Delay = 0;
    [SerializeField] Ease m_Ease = Ease.OutQuad;
    [SerializeField] int m_Loops = 1;
    [SerializeField] LoopType m_LoopType = LoopType.Restart;
    [SerializeField] UpdateType m_UpdateType = UpdateType.Normal;
    [SerializeField] bool m_Snapping = false;
    [SerializeField] UnityEvent m_OnPlay = null;
    [SerializeField] UnityEvent m_OnUpdate = null;
    [SerializeField] UnityEvent m_OnComplete = null;

    private Tween m_Tween;
    private void Awake()
    {
        InitTween();
        if (m_PlayOnAwake) DOPlay();
    }

    private void InitTween()
    {
        foreach (var item in m_Sequence)
        {
            var useFromValue = item.UseFromValue;
            if (!useFromValue) continue;
            var targetCom = item.Target;
            var resetValue = item.FromValue;
            switch (item.AnimationType)
            {
                case DOTweenType.DOMove:
                    {
                        (targetCom as Transform).position = resetValue;
                        break;
                    }
                case DOTweenType.DOMoveX:
                    {
                        (targetCom as Transform).SetPositionX(resetValue.x);
                        break;
                    }
                case DOTweenType.DOMoveY:
                    {
                        (targetCom as Transform).SetPositionY(resetValue.x);
                        break;
                    }
                case DOTweenType.DOMoveZ:
                    {
                        (targetCom as Transform).SetPositionZ(resetValue.x);
                        break;
                    }
                case DOTweenType.DOLocalMove:
                    {
                        (targetCom as Transform).localPosition = resetValue;
                        break;
                    }
                case DOTweenType.DOLocalMoveX:
                    {
                        (targetCom as Transform).SetLocalPositionX(resetValue.x);
                        break;
                    }
                case DOTweenType.DOLocalMoveY:
                    {
                        (targetCom as Transform).SetLocalPositionY(resetValue.x);
                        break;
                    }
                case DOTweenType.DOLocalMoveZ:
                    {
                        (targetCom as Transform).SetLocalPositionZ(resetValue.x);
                        break;
                    }
                case DOTweenType.DOAnchorPos:
                    {
                        (targetCom as RectTransform).anchoredPosition = resetValue;
                        break;
                    }
                case DOTweenType.DOAnchorPosX:
                    {
                        (targetCom as RectTransform).SetAnchoredPositionX(resetValue.x);
                        break;
                    }
                case DOTweenType.DOAnchorPosY:
                    {
                        (targetCom as RectTransform).SetAnchoredPositionY(resetValue.x);
                        break;
                    }
                case DOTweenType.DOAnchorPosZ:
                    {
                        (targetCom as RectTransform).SetAnchoredPosition3DZ(resetValue.x);
                        break;
                    }
                case DOTweenType.DOAnchorPos3D:
                    {
                        (targetCom as RectTransform).anchoredPosition3D = resetValue;
                        break;
                    }
                case DOTweenType.DOColor:
                    {
                        (targetCom as UnityEngine.UI.Graphic).color = resetValue;
                        break;
                    }
                case DOTweenType.DOFade:
                    {
                        (targetCom as UnityEngine.UI.Graphic).SetColorAlpha(resetValue.x);
                        break;
                    }
                case DOTweenType.DOCanvasGroupFade:
                    {
                        (targetCom as UnityEngine.CanvasGroup).alpha = resetValue.x;
                        break;
                    }
                case DOTweenType.DOValue:
                    {
                        (targetCom as UnityEngine.UI.Slider).value = resetValue.x;
                        break;
                    }
                case DOTweenType.DOSizeDelta:
                    {
                        (targetCom as RectTransform).sizeDelta = resetValue;
                        break;
                    }
                case DOTweenType.DOFillAmount:
                    {
                        (targetCom as UnityEngine.UI.Image).fillAmount = resetValue.x;
                        break;
                    }
                case DOTweenType.DOFlexibleSize:
                    {
                        (targetCom as LayoutElement).SetFlexibleSize(resetValue);
                        break;
                    }
                case DOTweenType.DOMinSize:
                    {
                        (targetCom as LayoutElement).SetMinSize(resetValue);
                        break;
                    }
                case DOTweenType.DOPreferredSize:
                    {
                        (targetCom as LayoutElement).SetPreferredSize(resetValue);
                        break;
                    }
                case DOTweenType.DOScale:
                    {
                        (targetCom as Transform).localScale = resetValue;
                        break;
                    }
                case DOTweenType.DOScaleX:
                    {
                        (targetCom as Transform).SetLocalScaleX(resetValue.x);
                        break;
                    }
                case DOTweenType.DOScaleY:
                    {
                        (targetCom as Transform).SetLocalScaleY(resetValue.x);
                        break;
                    }
                case DOTweenType.DOScaleZ:
                    {
                        (targetCom as Transform).SetLocalScaleZ(resetValue.z);
                        break;
                    }
                case DOTweenType.DORotate:
                    {
                        (targetCom as Transform).eulerAngles = resetValue;
                        break;
                    }
                case DOTweenType.DOLocalRotate:
                    {
                        (targetCom as Transform).localEulerAngles = resetValue;
                        break;
                    }
            }
        }
    }
    private Tween CreateTween(bool reverse = false)
    {
        if (m_Sequence == null || m_Sequence.Length == 0)
        {
            return null;
        }
        var sequence = DOTween.Sequence();
        if (reverse)
        {
            for (int i = m_Sequence.Length - 1; i >= 0; i--)
            {
                var item = m_Sequence[i];
                var tweener = item.CreateTween(reverse);
                if (tweener == null)
                {
                    Debug.LogErrorFormat("Tweener is null. Index:{0}, Animation Type:{1}, Component Type:{2}", i, item.AnimationType, item.Target == null ? "null" : item.Target.GetType().Name);
                    continue;
                }
                switch (item.AddType)
                {
                    case AddType.Append:
                        sequence.Append(tweener);
                        break;
                    case AddType.Join:
                        sequence.Join(tweener);
                        break;
                }
            }
        }
        else
        {
            for (int i = 0; i < m_Sequence.Length; i++)
            {
                var item = m_Sequence[i];
                var tweener = item.CreateTween(reverse);
                if (tweener == null)
                {
                    Debug.LogErrorFormat("Tweener is null. Index:{0}, Animation Type:{1}, Component Type:{2}", i, item.AnimationType, item.Target == null ? "null" : item.Target.GetType().Name);
                    continue;
                }
                switch (item.AddType)
                {
                    case AddType.Append:
                        sequence.Append(tweener);
                        break;
                    case AddType.Join:
                        sequence.Join(tweener);
                        break;
                }
            }
        }
        sequence.SetEase(m_Ease).SetUpdate(m_UpdateType).SetLoops(m_Loops, m_LoopType).SetDelay(m_Delay);
        if (m_OnPlay != null) sequence.OnPlay(m_OnPlay.Invoke);
        if (m_OnUpdate != null) sequence.OnUpdate(m_OnUpdate.Invoke);
        if (m_OnComplete != null) sequence.OnComplete(m_OnComplete.Invoke);
        sequence.SetAutoKill(true);
        return sequence;
    }
    public void Play()
    {
        DOPlay();
    }
    public Tween DOPlay()
    {
        m_Tween = CreateTween();
        return m_Tween?.Play();
    }

    public Tween DORewind()
    {
        m_Tween = CreateTween(true);
        return m_Tween?.Play();
    }

    public void DOComplete(bool withCallback = false)
    {
        m_Tween?.Complete(withCallback);
    }

    public void DOKill()
    {
        m_Tween?.Kill();
        m_Tween = null;
    }

    public enum DOTweenType
    {
        DOMove,
        DOMoveX,
        DOMoveY,
        DOMoveZ,

        DOLocalMove,
        DOLocalMoveX,
        DOLocalMoveY,
        DOLocalMoveZ,

        DOScale,
        DOScaleX,
        DOScaleY,
        DOScaleZ,

        DORotate,
        DOLocalRotate,

        DOAnchorPos,
        DOAnchorPosX,
        DOAnchorPosY,
        DOAnchorPosZ,
        DOAnchorPos3D,


        DOColor,
        DOFade,
        DOCanvasGroupFade,
        DOFillAmount,
        DOFlexibleSize,
        DOMinSize,
        DOPreferredSize,
        DOSizeDelta,
        DOValue
    }

    [Serializable]
    public class SequenceAnimation
    {
        public AddType AddType = AddType.Append;
        public DOTweenType AnimationType = DOTweenType.DOMove;
        public Component Target = null;
        public Vector4 ToValue = Vector4.zero;

        public bool UseToTarget = false;
        public Component ToTarget = null;

        public bool UseFromValue = false;
        public Vector4 FromValue = Vector4.zero;
        public bool SpeedBased = false;
        public float DurationOrSpeed = 1;
        public float Delay = 0;
        public UpdateType UpdateType = UpdateType.Normal;
        public bool CustomEase = false;
        public AnimationCurve EaseCurve;
        public Ease Ease = Ease.OutQuad;
        public int Loops = 1;
        public LoopType LoopType = LoopType.Restart;
        public bool Snapping = false;
        public UnityEvent OnPlay = null;
        public UnityEvent OnUpdate = null;
        public UnityEvent OnComplete = null;
        public Tween CreateTween(bool reverse)
        {
            Tween result = null;
            float duration = this.DurationOrSpeed;

            switch (AnimationType)
            {
                case DOTweenType.DOMove:
                    {
                        var transform = Target as Transform;
                        Vector3 targetValue = UseToTarget ? (ToTarget as Transform).position : ToValue;
                        Vector3 startValue = UseFromValue ? FromValue : transform.position;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.position = startValue;
                        if (SpeedBased)
                            duration = Vector3.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = transform.DOMove(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOMoveX:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).position.x : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.position.x;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetPositionX(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOMoveX(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOMoveY:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).position.y : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.position.y;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetPositionY(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOMoveY(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOMoveZ:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).position.z : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.position.z;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetPositionZ(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOMoveZ(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOLocalMove:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localPosition : (Vector3)ToValue;
                        var startValue = UseFromValue ? (Vector3)FromValue : transform.localPosition;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.localPosition = startValue;
                        if (SpeedBased)
                            duration = Vector3.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = transform.DOLocalMove(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOLocalMoveX:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localPosition.x : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.localPosition.x;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetLocalPositionX(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOLocalMoveX(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOLocalMoveY:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localPosition.y : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.localPosition.y;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetLocalPositionY(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOLocalMoveY(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOLocalMoveZ:
                    {
                        var transform = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localPosition.z : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : transform.localPosition.z;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        transform.SetLocalPositionZ(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = transform.DOLocalMoveZ(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOScale:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localScale : (Vector3)ToValue;
                        var startValue = UseFromValue ? (Vector3)FromValue : com.localScale;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.localScale = startValue;
                        if (SpeedBased) duration = Vector3.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOScale(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOScaleX:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localScale.x : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.localScale.x;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetLocalScaleX(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOScaleX(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOScaleY:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localScale.y : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.localScale.y;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetLocalScaleY(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOScaleY(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOScaleZ:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localScale.z : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.localScale.z;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetLocalScaleZ(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOScaleZ(targetValue, duration);
                    }
                    break;
                case DOTweenType.DORotate:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).eulerAngles : (Vector3)ToValue;
                        var startValue = UseFromValue ? (Vector3)FromValue : com.eulerAngles;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.eulerAngles = startValue;
                        if (SpeedBased)
                            duration = GetEulerAnglesAngle(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DORotate(targetValue, duration, RotateMode.FastBeyond360);
                    }
                    break;
                case DOTweenType.DOLocalRotate:
                    {
                        var com = Target as Transform;
                        var targetValue = UseToTarget ? (ToTarget as Transform).localEulerAngles : (Vector3)ToValue;
                        var startValue = UseFromValue ? (Vector3)FromValue : com.localEulerAngles;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.localEulerAngles = startValue;
                        if (SpeedBased)
                            duration = GetEulerAnglesAngle(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOLocalRotate(targetValue, duration, RotateMode.FastBeyond360);
                    }
                    break;
                case DOTweenType.DOAnchorPos:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).anchoredPosition : (Vector2)ToValue;
                        var startValue = UseFromValue ? (Vector2)FromValue : rectTransform.anchoredPosition;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        rectTransform.anchoredPosition = startValue;
                        if (SpeedBased)
                            duration = Vector2.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOAnchorPos(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOAnchorPosX:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).anchoredPosition.x : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : rectTransform.anchoredPosition.x;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        rectTransform.SetAnchoredPositionX(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOAnchorPosX(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOAnchorPosY:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).anchoredPosition.y : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : rectTransform.anchoredPosition.y;
                        if (reverse)
                        {
                            var swapValue = startValue;
                            startValue = targetValue;
                            targetValue = swapValue;
                        }
                        rectTransform.SetAnchoredPositionY(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOAnchorPosY(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOAnchorPosZ:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).anchoredPosition3D.z : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : rectTransform.anchoredPosition3D.z;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        rectTransform.SetAnchoredPosition3DZ(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOAnchorPos3DZ(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOAnchorPos3D:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).anchoredPosition3D : (Vector3)ToValue;
                        var startValue = UseFromValue ? (Vector3)FromValue : rectTransform.anchoredPosition3D;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        rectTransform.anchoredPosition3D = startValue;
                        if (SpeedBased)
                            duration = Vector3.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOAnchorPos3D(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOSizeDelta:
                    {
                        var rectTransform = Target as RectTransform;
                        var targetValue = UseToTarget ? (ToTarget as RectTransform).sizeDelta : (Vector2)ToValue;
                        var startValue = UseFromValue ? (Vector2)FromValue : rectTransform.sizeDelta;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        rectTransform.sizeDelta = startValue;
                        if (SpeedBased)
                            duration = Vector2.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = rectTransform.DOSizeDelta(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOColor:
                    {
                        var com = Target as UnityEngine.UI.Graphic;
                        var targetValue = UseToTarget ? (ToTarget as UnityEngine.UI.Graphic).color : (Color)ToValue;
                        var startValue = UseFromValue ? (Color)FromValue : com.color;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.color = startValue;
                        if (SpeedBased)
                            duration = Vector4.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOColor(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOFade:
                    {
                        var com = Target as UnityEngine.UI.Graphic;
                        var targetValue = UseToTarget ? (ToTarget as UnityEngine.UI.Graphic).color.a : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.color.a;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetColorAlpha(startValue);
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOFade(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOCanvasGroupFade:
                    {
                        var com = Target as UnityEngine.CanvasGroup;
                        var targetValue = UseToTarget ? (ToTarget as UnityEngine.CanvasGroup).alpha : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.alpha;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.alpha = startValue;
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOFade(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOValue:
                    {
                        var com = Target as UnityEngine.UI.Slider;
                        var targetValue = UseToTarget ? (ToTarget as UnityEngine.UI.Slider).value : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.value;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.value = startValue;
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOValue(targetValue, duration, Snapping);
                    }
                    break;

                case DOTweenType.DOFillAmount:
                    {
                        var com = Target as UnityEngine.UI.Image;
                        var targetValue = UseToTarget ? (ToTarget as UnityEngine.UI.Image).fillAmount : ToValue.x;
                        var startValue = UseFromValue ? FromValue.x : com.fillAmount;
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.fillAmount = startValue;
                        if (SpeedBased)
                            duration = Mathf.Abs(targetValue - startValue) / this.DurationOrSpeed;
                        result = com.DOFillAmount(targetValue, duration);
                    }
                    break;
                case DOTweenType.DOFlexibleSize:
                    {
                        var com = Target as LayoutElement;
                        var targetValue = UseToTarget ? (ToTarget as LayoutElement).GetFlexibleSize() : (Vector2)ToValue;
                        var startValue = UseFromValue ? (Vector2)FromValue : com.GetFlexibleSize();
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetFlexibleSize(startValue);
                        if (SpeedBased)
                            duration = Vector2.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOFlexibleSize(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOMinSize:
                    {
                        var com = Target as LayoutElement;
                        var targetValue = UseToTarget ? (ToTarget as LayoutElement).GetMinSize() : (Vector2)ToValue;
                        var startValue = UseFromValue ? (Vector2)FromValue : com.GetMinSize();
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetMinSize(startValue);
                        if (SpeedBased)
                            duration = Vector2.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOMinSize(targetValue, duration, Snapping);
                    }
                    break;
                case DOTweenType.DOPreferredSize:
                    {
                        var com = Target as LayoutElement;
                        var targetValue = UseToTarget ? (ToTarget as LayoutElement).GetPreferredSize() : (Vector2)ToValue;
                        var startValue = UseFromValue ? (Vector2)FromValue : com.GetPreferredSize();
                        if (reverse)
                        {
                            (targetValue, startValue) = (startValue, targetValue);
                        }
                        com.SetPreferredSize(startValue);
                        if (SpeedBased)
                            duration = Vector2.Distance(targetValue, startValue) / this.DurationOrSpeed;
                        result = com.DOPreferredSize(targetValue, duration, Snapping);
                    }
                    break;
            }

            if (result != null)
            {
                result.SetAutoKill(true).SetTarget(Target.gameObject).SetLoops(Loops, LoopType).SetUpdate(UpdateType);
                if (Delay > 0) result.SetDelay(Delay);
                if (CustomEase) result.SetEase(EaseCurve);
                else result.SetEase(Ease);

                if (OnPlay != null) result.OnPlay(OnPlay.Invoke);
                if (OnUpdate != null) result.OnUpdate(OnUpdate.Invoke);
                if (OnComplete != null) result.OnComplete(OnComplete.Invoke);
            }
            return result;
        }
        public static float GetEulerAnglesAngle(Vector3 euler1, Vector3 euler2)
        {
            // 计算差值
            Vector3 delta = euler2 - euler1;
            delta.x = Mathf.DeltaAngle(euler1.x, euler2.x);
            delta.y = Mathf.DeltaAngle(euler1.y, euler2.y);
            delta.z = Mathf.DeltaAngle(euler1.z, euler2.z);

            float angle = Mathf.Sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
            return (angle + 360) % 360;
        }
    }
    public enum AddType
    {
        Append,
        Join
    }
}
相关推荐
界面开发小八哥2 小时前
DevExpress WPF中文教程:Grid - 如何移动和调整列大小?(二)
ui·.net·wpf·界面控件·devexpress·ui开发
超龄魔法少女6 小时前
[Unity] ShaderGraph动态修改Keyword Enum,实现不同效果一键切换
unity·技术美术·shadergraph
蔗理苦8 小时前
2024-12-24 NO1. XR Interaction ToolKit 环境配置
unity·quest3·xr toolkit
花生糖@8 小时前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
向宇it8 小时前
【从零开始入门unity游戏开发之——unity篇02】unity6基础入门——软件下载安装、Unity Hub配置、安装unity编辑器、许可证管理
开发语言·unity·c#·编辑器·游戏引擎
徐小夕@趣谈前端8 小时前
可视化大屏编辑器, 开源!
编辑器
天天进步20158 小时前
TipTap编辑器:现代化的富文本编辑解决方案
编辑器
向宇it12 小时前
【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么、主流的游戏引擎、为什么选择Unity
开发语言·unity·c#·游戏引擎
weixin_4231961713 小时前
VSCode+WSL作为IDE开发和管理深度学习项目
ide·vscode·编辑器
乐闻x13 小时前
VSCode 插件开发实战(八):创建和管理任务 Task
ide·vscode·编辑器