Demigiant / dotween

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

OnKill is called late when calling Kill from tween callback #664

Open Decavoid opened 10 months ago

Decavoid commented 10 months ago

Description:

When killing tweens inside another OnComplete callback, the corresponding OnKill callback of the tween to be killed is not called immediately, synchronously. There is a check TweenManager.isUpdateLoop inside Tween.Kill that prevents it. OnKill is called at some time in the future instead. As recommended in the docs, when recycling is enabled, we null the tween in OnKill callback: .OnKill(()=> myTweenReference = null). The intention is to immediately kill the active tween and start another one as a replacement, assigning the new tween to the same variable. The current behavior (OnKill is late) is nulling the replacement tween reference, thus the replacement tween can no longer be killed if needed.

Proposal:

Raise OnKill callback always synchronously in Kill(), regardless of TweenManager.isUpdateLoop check.

Test code:

Recycle tweens: enabled. Safe mode: true.

using DG.Tweening;
using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    private float myFloat = 0;
    private Tweener floatTween;

    private void Awake()
    {
        Application.targetFrameRate = 60;
        DOTween.Init(recycleAllByDefault: true, useSafeMode: true, LogBehaviour.Verbose);
    }

    private void Update()
    {
        if (Time.frameCount == 100)
            StartTest();
    }

    private void StartTest()
    {
        Log("StartTest");
        StartIntTween(duration: 2, endValue: 10);
        StartFloatTween(duration: 5, endValue: 100);
    }

    private void StartIntTween(float duration, int endValue)
    {
        Log($"Starting intTween, {duration}s, endValue:{endValue}...");
        int myInt = 0;
        DOTween.To(() => myInt, v => myInt = v, endValue, duration)
            .OnComplete(() =>
            {
                Log($"intTween OnComplete, final value:{myInt}, killing floatTween...");
                floatTween.Kill(); // expecting floatTween is null after this

                if (floatTween != null)
                    Log($"floatTween is not null after Kill!", true);

                StartFloatTween(duration: 8, endValue: 500);
                StartCoroutine(Coroutine());
            });
    }

    private void StartFloatTween(float duration, float endValue)
    {
        Log($"Starting floatTween, {duration}s, endValue:{endValue}...");
        floatTween = DOTween.To(() => myFloat, v => myFloat = v, endValue, duration)
            .OnComplete(() => Log($"floatTween OnComplete, final value: {myFloat}"))
            .OnKill(() =>
            {
                Log("floatTween OnKill, setting floatTween to null");
                floatTween = null;
            });
    }

    private IEnumerator Coroutine()
    {
        Log("Coroutine started, waiting 1s...");
        yield return new WaitForSeconds(1);

        Log($"Coroutine is killing floatTween...");

        if (floatTween != null)
        {
            Log("floatTween is not null, killing...");
            floatTween.Kill();
            Log($"floatTween is killed successfully, floatTween is null:{floatTween == null}");
        }
        else
        {
            Log("floatTween is null, can't kill!", true);
        }
    }

    private void Log(string message, bool warn = false)
    {
        var msg = $"Frame:{Time.frameCount} {message}";
        if (warn)
            Debug.LogWarning(msg);
        else
            Debug.Log(msg);
    }
}

Expected console output:

DOTWEEN ► DOTween initialization (useSafeMode: True, recycling: ON, logBehaviour: Verbose)
Frame:100 StartTest
Frame:100 Starting intTween, 2s, endValue:10...
Frame:100 Starting floatTween, 5s, endValue:100...
Frame:220 intTween OnComplete, final value:10, killing floatTween...
Frame:220 floatTween OnKill, setting floatTween to null
Frame:220 Starting floatTween, 8s, endValue:500...
Frame:220 Coroutine started, waiting 1s...
Frame:281 Coroutine is killing floatTween...
Frame:281 floatTween is not null, killing...
Frame:281 floatTween OnKill, setting floatTween to null
Frame:281 floatTween is killed successfully, floatTween is null:true

Actual console output:

DOTWEEN ► DOTween initialization (useSafeMode: True, recycling: ON, logBehaviour: Verbose)
Frame:100 StartTest
Frame:100 Starting intTween, 2s, endValue:10...
Frame:100 Starting floatTween, 5s, endValue:100...
Frame:220 intTween OnComplete, final value:10, killing floatTween...
Frame:220 floatTween is not null after Kill!
Frame:220 Starting floatTween, 8s, endValue:500...
Frame:220 Coroutine started, waiting 1s...
Frame:220 floatTween OnKill, setting floatTween to null
Frame:281 Coroutine is killing floatTween...
Frame:281 floatTween is null, can't kill!
Frame:701 floatTween OnComplete, final value: 500
Frame:701 floatTween OnKill, setting floatTween to null
santelelle commented 9 months ago

I have a similar issue when killing a tween inside a sequence callback

Sequence sequence = DOTween.Sequence();
sequence.AppendCallback(() => {
   DOTween.Kill("MyTween"))
   // <- after the Kill line, the OnKill() method of MyTween is not yet called
};