Demigiant / dotween

A Unity C# animation engine. HOTween v2
http://dotween.demigiant.com
Other
2.29k stars 344 forks source link

UI Tweening works unreliably #679

Closed sima995 closed 2 months ago

sima995 commented 2 months ago

Hi! I have been using DOTween for quite a while and recently I've started having issues with tweening UI elements in Unity. The code I'm having issues with is:

        public void SetFill(bool value)
        {
            IsFilled = value;
            tween = fill.rectTransform.DOScale(value ? Vector3.one : Vector3.zero, 2f);
        }

        [Button]
        public void Toggle()
        {
            SetFill(!IsFilled);
        }

I attached a short video of the behaviour. It's all strange to me because the behaviour can differ based on who calles the SetFill() method. For example if it's not called from the editor the scaling up animation happens instantly while the scaling down works as expected. I've tried killing the tween before playing a new one but it didn't help. I also logged a test message in the above code to make sure it's not called multiple times, but it was not.

Is this possibly a bug, or am I doing something wrong? I'm using Unity 2022.3.4f1

https://github.com/Demigiant/dotween/assets/83577850/8b956832-efac-45c9-b7cc-65e3c2a73368

Decavoid commented 2 months ago

You are doing something wrong. Here is a test case that works as expected.

TestScene.cs:

using DG.Tweening;
using UnityEngine;

public class TestScene : MonoBehaviour
{
    private void Awake()
    {
        DOTween.Init(true, true, LogBehaviour.Verbose);
    }
}

CapacityUI.cs:

using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;

public class CapacityUI : MonoBehaviour
{
    [SerializeField] Image fill;

    private bool isFilled = true;
    private Tweener tween;

    public void SetFill(bool value)
    {
        isFilled = value;
        tween = fill.rectTransform.DOScale(value ? Vector3.one : Vector3.zero, 2f);
    }

    public void Toggle()
    {
        SetFill(!isFilled);
    }
}

CapacityUIEditor.cs:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(CapacityUI))]
public class CapacityUIEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
        var myTarget = (CapacityUI)target;
        if (GUILayout.Button("Toggle"))
            myTarget.Toggle();
    }
}

https://github.com/Demigiant/dotween/assets/1127000/5f8bff50-3245-49b7-b950-2f110fc304fd

sima995 commented 2 months ago

You are doing something wrong. Here is a test case that works as expected.

TestScene.cs:

using DG.Tweening;
using UnityEngine;

public class TestScene : MonoBehaviour
{
    private void Awake()
    {
        DOTween.Init(true, true, LogBehaviour.Verbose);
    }
}

CapacityUI.cs:

using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;

public class CapacityUI : MonoBehaviour
{
    [SerializeField] Image fill;

    private bool isFilled = true;
    private Tweener tween;

    public void SetFill(bool value)
    {
        isFilled = value;
        tween = fill.rectTransform.DOScale(value ? Vector3.one : Vector3.zero, 2f);
    }

    public void Toggle()
    {
        SetFill(!isFilled);
    }
}

CapacityUIEditor.cs:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(CapacityUI))]
public class CapacityUIEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
        var myTarget = (CapacityUI)target;
        if (GUILayout.Button("Toggle"))
            myTarget.Toggle();
    }
}

Recording.2024-04-15.185641.mp4

Are you using the same Unity version?

Decavoid commented 2 months ago

You are doing something wrong. Here is a test case that works as expected. TestScene.cs:

using DG.Tweening;
using UnityEngine;

public class TestScene : MonoBehaviour
{
    private void Awake()
    {
        DOTween.Init(true, true, LogBehaviour.Verbose);
    }
}

CapacityUI.cs:

using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;

public class CapacityUI : MonoBehaviour
{
    [SerializeField] Image fill;

    private bool isFilled = true;
    private Tweener tween;

    public void SetFill(bool value)
    {
        isFilled = value;
        tween = fill.rectTransform.DOScale(value ? Vector3.one : Vector3.zero, 2f);
    }

    public void Toggle()
    {
        SetFill(!isFilled);
    }
}

CapacityUIEditor.cs:

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(CapacityUI))]
public class CapacityUIEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
        var myTarget = (CapacityUI)target;
        if (GUILayout.Button("Toggle"))
            myTarget.Toggle();
    }
}

Recording.2024-04-15.185641.mp4

Are you using the same Unity version?

Unity 2022.3.24f1 DOTween 1.2.765

Demigiant commented 2 months ago

I think this is a problem of tweens overlapping each other, meaning that you're creating a tween to either zero or one while the opposite tween is still playing, and thus the previous tween will still have control over the animation, which will follow it. Until it ends and the animation suddenly snaps to the new tween (which in the meantime has been playing even if overridden, and so will not be at time 0 but further).

I recommend to store a reference to the tween you create and kill it before creating a new one (always do that especially when using a tween on the same target). Or, even better, store the tween once and set its autoKill behaviour to false, then just play it forward (tween.PlayForward) and backwards (tween.PlayBackwards). If you do the latter, remember to kill that tween manually since it won't be killed automatically when it completes (best way is to kill all tweens created by a MonoBehaviour inside that MonoBehaviour OnDestroy).

sima995 commented 2 months ago

I think this is a problem of tweens overlapping each other, meaning that you're creating a tween to either zero or one while the opposite tween is still playing, and thus the previous tween will still have control over the animation, which will follow it. Until it ends and the animation suddenly snaps to the new tween (which in the meantime has been playing even if overridden, and so will not be at time 0 but further).

I recommend to store a reference to the tween you create and kill it before creating a new one (always do that especially when using a tween on the same target). Or, even better, store the tween once and set its autoKill behaviour to false, then just play it forward (tween.PlayForward) and backwards (tween.PlayBackwards). If you do the latter, remember to kill that tween manually since it won't be killed automatically when it completes (best way is to kill all tweens created by a MonoBehaviour inside that MonoBehaviour OnDestroy).

Thanks for the reply. As I mentioned, I tried killing the tween before playing it like this:

    public class CapacityUI : MonoBehaviour
    {
        [SerializeField] private Image fill;

        public bool IsFilled { get; private set; }
        Tween tween;

        private void Start()
        {
            SetFill(false);
        }

        public void SetFill(bool value)
        {
            IsFilled = value;
            if(tween != null)
            {
                DOTween.Kill(tween);
            }
            tween = fill.rectTransform.DOScale(value ? 1 : 0, 2f);
        }

        [Button]
        public void Toggle()
        {
            SetFill(!IsFilled);
        }
    }

It's still not working as intended. Is there anything else that might typically go wrong in similar situations?

Decavoid commented 2 months ago

I think this is a problem of tweens overlapping each other, meaning that you're creating a tween to either zero or one while the opposite tween is still playing, and thus the previous tween will still have control over the animation, which will follow it. Until it ends and the animation suddenly snaps to the new tween (which in the meantime has been playing even if overridden, and so will not be at time 0 but further). I recommend to store a reference to the tween you create and kill it before creating a new one (always do that especially when using a tween on the same target). Or, even better, store the tween once and set its autoKill behaviour to false, then just play it forward (tween.PlayForward) and backwards (tween.PlayBackwards). If you do the latter, remember to kill that tween manually since it won't be killed automatically when it completes (best way is to kill all tweens created by a MonoBehaviour inside that MonoBehaviour OnDestroy).

Thanks for the reply. As I mentioned, I tried killing the tween before playing it like this:

    public class CapacityUI : MonoBehaviour
    {
        [SerializeField] private Image fill;

        public bool IsFilled { get; private set; }
        Tween tween;

        private void Start()
        {
            SetFill(false);
        }

        public void SetFill(bool value)
        {
            IsFilled = value;
            if(tween != null)
            {
                DOTween.Kill(tween);
            }
            tween = fill.rectTransform.DOScale(value ? 1 : 0, 2f);
        }

        [Button]
        public void Toggle()
        {
            SetFill(!IsFilled);
        }
    }

It's still not working as intended. Is there anything else that might typically go wrong in similar situations?

Any other code attempts to drive this image at the same time: ContentSizeFitter HorizontalLayoutGroup VerticalLayoutGroup etc

sima995 commented 2 months ago

Finally figured out what the problem was. I searched through the entire codebase for occurrences of Dotween methods and I found a DOTween.KillAll(image) hanging around in one my scripts that tied into the process of calling the SetFill() method. The funny part is that I wasn't aware that KillAll's parameter was a bool and for some reason I thought that it would kill all tweens on the passed object.

Thanks for the replies though and sorry for opening an issue.