【问题标题】:Cancelling Coroutine when Home button presseddown or returned main menu按下主页按钮或返回主菜单时取消协程
【发布时间】:2019-10-26 03:20:19
【问题描述】:

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

    }

这是我按下主页按钮/返回主菜单时捕捉到的地方。当抓住它并弹出暂停菜单时

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# unity3d time countdown coroutine


    【解决方案1】:

    在这种情况下,您错误地使用了CancellationTokenCancellationToken 是一个包装 CancellationTokenSource 的结构,如下所示:

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

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

    处理取消的典型方法是创建一个CancellationTokenSource 并传递它的Token。每当您想取消它时,您只需在CancellationTokenSource 上调用.Cancel() 方法。采用这种方式的原因是,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 docs 的前几行中提取:

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

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

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

    不过,最终选择权在你。

    【讨论】:

      【解决方案2】:

      主要问题

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

      因此,按照您的方法的逻辑,您可能只想拥有一个List&lt;bool&gt; cancellationRequests 并使用ref keyword 传递它们。

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

      FLAW => 这些解决方案不断将内容添加到 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方法之后可以多次调用协程)。


      解决方案

      使用静态事件而不是列表

      要删除列表并使类更加解耦,您可以使用静态事件,在调用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 而不是 Unity (它们也更多 performant )。

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

      单次协程启动

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

      使用特定标签停止所有 COROUTINE

      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 解决方案的细节,它可能会混淆答案,并且超出了这个问题的范围。

      【讨论】:

      • “CancellationToken 主要用于任务” - MSDN docs 说它们用于“异步或长时间运行的同步操作”。 “您不需要 CancellationToken 的开销” - 什么开销?它们旨在非常轻巧。您是否真的对他们的表现进行了分析并发现了“开销”?您建议的解决方案是合理的,但我认为可读性要差得多。
      • 从我读到的代码中,我们只需要一个布尔值。取消令牌肯定比布尔具有更多的开销。进一步注意任务和协程是不同的。我倾向于同意this forum:“协程对于在多个帧上执行方法很有用。异步方法对于在给定任务完成后执行方法很有用。”无论如何,如果你想使用 CancellationToken 我不会创建 CancellationTokenSource 列表,而是创建一组 CancellationToken 仅连接到一个源。
      • 当然,它比布尔值有更多的开销。但它背后也有更多的可读性和意图。 “开销”是如此微不足道,以至于我认为它甚至不值得一提。我知道 Task 和 Coroutine 是不同的,我不认为我说了什么让你有不同的想法。我只是指出“CancellationToken 主要用于任务”是不正确的。是的,我同意在 OP 的情况下,列出它们是多余的,我确实编辑了我的答案以包含它。
      • 我同意您的解决方案 1,但我认为您的解决方案(甚至问题本身)也存在可读性问题。这仍然不是我主要关心的问题。他们最大的问题是他们都没有清除列表,那就是内存泄漏。所以我添加了解决方案 2 和解决方案 X 作为更好的版本。
      • 是的,我同意这是一个大问题,在那个问题上很明显。
      【解决方案3】:

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

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

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

      【讨论】:

      • 我的协程包含的不仅仅是我希望它们运行的​​技能。所以在这种情况下,StopCoroutine 对我来说效果不佳。
      猜你喜欢
      • 2016-12-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-14
      • 1970-01-01
      相关资源
      最近更新 更多