按下主页按钮或返回主菜单时取消协程

Cancelling Coroutine when Home button presseddown or returned main menu

我正在做的一些借口;我目前通过在协程中设置 interactable = false 来锁定我的技能按钮。通过 textmeshpro 显示剩余秒数的文本,并在倒计时结束时将其设置为停用。但是当按下主页按钮/返回主菜单时我遇到了问题。我想刷新我的按钮冷却时间并在按下时停止协程。但它一直处于锁定状态。

这是我的冷却协程

static List<CancellationToken> cancelTokens = new List<CancellationToken>();

...

public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
    {
        try
        {
            this.currentCooldownDuration = countdownValue;
            // Deactivate myButton
            this.myButton.interactable = false;
            //activate text to show remaining cooldown seconds
            this.m_Text.SetActive(true);
            while (this.currentCooldownDuration > 0 && !cancellationToken.IsCancellationRequested)
            {

                this.m_Text.GetComponent<TMPro.TextMeshProUGUI>().text = this.currentCooldownDuration.ToString(); //Showing the Score on the Canvas
                yield return new WaitForSeconds(1.0f);
                this.currentCooldownDuration--;

            }
        }
        finally
        {
            // deactivate text and Reactivate myButton
            // deactivate text
            this.m_Text.SetActive(false);
            // Reactivate myButton
            this.myButton.interactable = true;
        }

    }
static public void cancelAllCoroutines()
   {
       Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
       foreach (CancellationToken ca in cancelTokens)
       {
           ca.IsCancellationRequested = true;
       }
   }


void OnButtonClick()
    {

    CancellationToken cancelToken = new CancellationToken();
        cancelTokens.Add(cancelToken);

        Coroutine co;
        co = StartCoroutine(StartCountdown(cooldownDuration, cancelToken));
        myCoroutines.Add(co);

    }

这是我在主页按钮 pressed/returned 主菜单时看到的地方。当抓住它并弹出 pauseMenu

public void PauseGame()
    {
        GameObject menu = Instantiate(PauseMenu);
        menu.transform.SetParent(Canvas.transform, false);
        gameManager.PauseGame();
        EventManager.StartListening("ReturnMainMenu", (e) =>
        {
            Cooldown.cancelAllCoroutines();
            Destroy(menu);
            BackToMainMenu();
            EventManager.StopListening("ReturnMainMenu");
        });
        ...

比赛暂停时我也停止计时

public void PauseGame() {
        Time.timeScale = 0.0001f;
    }

Unity 有 StopCoroutine() 专门用于提前结束协程。

你会想要创建一个函数,当你想像这样重置它们时,你可以调用每个技能按钮对象:

void resetButton()
{
    StopCoroutine(StartCountdown);
    this.currentCooldownDuration = 0;
    this.m_Text.SetActive(false);
    this.myButton.interactable = true;
}

在这种情况下,您使用的 CancellationToken 不正确。 CancellationToken 是一个像这样包装 CancellationTokenSource 的结构:

public bool IsCancellationRequested 
{
    get
    {
        return source != null && source.IsCancellationRequested;
    }
}

因为它是一个结构,所以它会按值传递,这意味着您存储在列表中的那个是不同的实例作为你的 Coroutine 拥有的那个。

处理取消的典型方法是创建一个 CancellationTokenSource 并传递它的 Token。每当您想取消它时,只需调用 CancellationTokenSource 上的 .Cancel() 方法即可。这样做的原因是 CancellationToken 只能 通过 'source' 引用而不是由令牌的消费者取消。

在您的情况下,您正在创建一个根本没有来源的令牌,因此我建议进行以下更改:

首先,将您的 cancelTokens 列表更改为:

List<CancellationTokenSource>

接下来,将您的 OnButtonClick() 方法更改为如下所示:

public void OnButtonClick()
{
    // You should probably call `cancelAllCoroutines()` here  
    cancelAllCoroutines();

    var cancellationTokenSource = new CancellationTokenSource();
    cancelTokens.Add(cancellationTokenSource);
    Coroutine co = StartCoroutine(StartCountdown(cooldownDuration, cancellationTokenSource.Token));
    myCoroutines.Add(co);
}

最后,将您的 cancelAllCoroutines() 方法更改为:

public static void CancelAllCoroutines()
{
   Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
   foreach (CancellationTokenSource ca in cancelTokens)
   {
       ca.Cancel();
   }

   // Clear the list as @Jack Mariani mentioned
   cancelTokens.Clear();
}

我建议阅读有关 Cancellation Tokens or alternatively, as @JLum suggested, use the StopCoroutine Unity 提供的方法的文档。

编辑: 忘了说了,建议CancallationTokenSources在不用的时候就处理掉,保证不会出现内存泄露。我建议在 MonoBehaviourOnDestroy() 挂钩中这样做:

private void OnDestroy()
{
    foreach(var source in cancelTokens)
    {
        source.Dispose();
    }
}

编辑 2: 正如@Jack Mariani 在他的回答中提到的,在这种情况下,多个 CancellationTokenSources 是矫枉过正的。它真正允许您做的就是对取消哪些 Coroutine 有更多 fine-grained 控制。在这种情况下,您将一次性全部取消它们,所以是的,一种优化方法是只创建其中一个。这里可以进行多种优化,但它们超出了这个问题的范围。我没有包括它们,因为我觉得它会使这个答案变得多余。

但是,我会争论他关于 CancellationToken 是 'mostly intended for Task' 的观点。直接从 MSDN docs:

的前几行中提取

Starting with the .NET Framework 4, the .NET Framework uses a unified model for cooperative cancellation of asynchronous or long-running synchronous operations. This model is based on a lightweight object called a cancellation token

CancellationTokens 是轻量级对象。大多数情况下,它们只是引用 CancellationTokenSource 的简单 Structs。他的回答中提到的 'overhead' 可以忽略不计,在我看来,考虑到可读性和意图时,这是完全值得的。

可以 通过索引传递 booleans 负载或使用 string 文字订阅事件,这些方法会起作用。 但代价是什么?令人困惑和 difficult-to-read 代码?我会说不值得。

不过最终选择权在你。

主要问题

在你的问题中,你只使用了 bool cancellationToken.IsCancellationRequested 而没有取消令牌的其他功能。

因此,按照您的方法逻辑,您可能只想拥有一个 List<bool> cancellationRequests 并使用 ref keyword.

传递它们

不过,我不会继续采用该逻辑,也不会采用 Darren Ruane 提出的逻辑,因为 它们有一个主要缺陷

FLAW => 这些解决方案一直在向 2 个列表 cancelTokens.Add(...)myCoroutines.Add(co) 添加内容,但从未清除它们。


public void OnButtonClick()
{
    ...other code
    //this never get cleared
    cancelTokens.Add(cancelToken);
    ...other code
    //this never get cleared
    myCoroutines.Add(co);
}

如果你想这样下去你可以手动删除它们,但这很棘手,因为你永远不知道什么时候需要它们(协程可以在 CancelAllCoroutines 方法之后调用很多帧)。


解决方案

使用静态事件而不是列表

要删除列表并使 class 更加解耦,您可以使用静态事件,在调用 PauseGame 方法的脚本中创建和调用。

//the event somewhere in the script
public static event Action OnCancelCooldowns;

public void PauseGame()
{
    ...your code here, with no changes...
    EventManager.StartListening("ReturnMainMenu", (e) =>
    {
        //---> Removed ->Cooldown.cancelAllCoroutines();
        //replaced by the event
        OnCancelCooldowns?.Invoke();

        ...your code here, with no changes...
    });
    ...

您将在协程中监听静态事件。

public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
{
    try
    {
        bool wantToStop = false;
        //change YourGameManager with the name of the script with your PauseGame method.
        YourGameManager.OnCancelCooldowns += () => wantToStop = true;

        ...your code here, with no changes...

        while (this.currentCooldownDuration > 0 && !wantToStop)
        {
            ...your code here, with no changes...
        }
    }
    finally
    {
        ...your code here, with no changes...
    }
}

更简单的解决方案

或者您可以保持简单并使用 MEC Coroutines instead of Unity ones (they are also more performant)。

MEC Free 提供了完全解决问题的标签功能。

单个协程启动

void OnButtonClick() => Timing.RunCoroutine(CooldownCoroutine, "CooldownTag");  

停止所有带有特定标签的协程

public void PauseGame()
{
    ...your code here, with no changes...
    EventManager.StartListening("ReturnMainMenu", (e) =>
    {
        //---> Removed ->Cooldown.cancelAllCoroutines();
        //replaced by this
        Timing.KillCoroutines("CooldownTag");

        ...your code here, with no changes...
    });
    ...

进一步 notes on tags(请考虑 MEC free 只有标签而不是层,但在您的用例中您只需要标签)。

编辑: 经过深思熟虑我决定删除 bool 解决方案的细节,这可能会混淆答案并且超出了这个问题的范围。