首页 > 解决方案 > 按下主页按钮或返回主菜单时取消协程

问题描述

我正在做的一些借口;我目前正在通过在协程中设置 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);

    }

这是我按下主页按钮/返回主菜单时捕捉到的地方。当抓住它并弹出 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;
    }

标签: c#unity3dtimecountdowncoroutine

解决方案


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

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

因为它是一个结构,所以它通过 value传递,这意味着您存储在列表中实例与您Coroutine拥有的实例不同。

处理取消的典型方法是创建一个CancellationTokenSource并传递它Token。每当您想取消它时,只需.Cancel()调用CancellationTokenSource. 采用这种方式的原因是只能CancellationToken通过“源”引用而不是令牌的消费者来取消。

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

首先,将您的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的文档,或者像 @JLum 建议的那样,使用 Unity 提供的StopCoroutine方法。

编辑:我忘了提到,建议CancallationTokenSources在不再使用时将其处理掉,以确保不会发生内存泄漏。我建议您在OnDestroy()挂钩中执行此操作,MonoBehaviour如下所示:

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

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

但是,我会争论他关于CancellationToken“主要用于任务”的观点。直接从 MSDN文档的前几行中提取:

从 .NET Framework 4 开始,.NET Framework 使用统一模型来协作取消异步或长时间运行的同步操作。该模型基于称为取消令牌的轻量级对象

CancellationTokens是轻量级的物体。在大多数情况下,它们只是简单Structs地引用 a CancellationTokenSource。他的回答中提到的“开销”可以忽略不计,在我看来,在考虑可读性和意图时完全值得。

可以使用索引传递大量负载或使用文字booleans订阅事件,这些方法将起作用。string但代价是什么?令人困惑且难以阅读的代码?我会说不值得。

选择最终是你的。


推荐阅读