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

时间:2019-06-11 16:34:09

标签: c# unity3d time countdown coroutine

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

3 个答案:

答案 0 :(得分:3)

在这种情况下,您使用的CancellationToken错误。 CancellationToken是这样包装CancellationTokenSource的结构:

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

由于它是一个结构,它会按值传递给 ,这意味着您存储在列表中的那个与您的{{1 }}。

处理取消的典型方法是创建一个CancellationTokenSource并传递其Coroutine。每当您想取消它时,只需在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()

我建议阅读Cancellation Tokens上的文档,或者按照@JLum的建议,使用Unity提供的StopCoroutine方法。

编辑: 我忘记提及了,建议在不再使用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(); } 时将其丢弃,以确保不会发生内存泄漏。我建议在CancallationTokenSources的{​​{3}}钩子中这样做,就像这样:

MonoBehaviour

编辑2: 正如@Jack Mariani在回答中提到的那样,在这种情况下,多个private void OnDestroy() { foreach(var source in cancelTokens) { source.Dispose(); } } 是过大的。它真正允许您执行的操作是对取消CancellationTokenSources的情况进行更细粒度的控制。在这种情况下,您要一次性取消它们,因此,一种优化将是仅创建其中一个。这里可以进行多种优化,但它们超出了此问题的范围。我之所以没有包含它们,是因为我觉得它会使这个答案超出了必要。

但是,我会争论他关于Coroutine被“主要用于Task”的观点。从MSDN OnDestroy()的前几行中直接拉出来:

  

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

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

可以传递带有索引的CancellationTokenSource负载,或使用booleans文字常量订阅事件,这些方法将起作用。 但是要花多少钱呢?令人困惑和难以阅读的代码?我会说不值得。

选择最终还是你的。

答案 1 :(得分:1)

主要问题

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

因此,按照方法的逻辑,您可能只想拥有一个List<bool> 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");  

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

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仅包含标签,没有图层,但在用例中只需要标签)。

编辑: 经过一番思考后,我决定删除bool解决方案的详细信息,这可能会使答案感到困惑,并且超出了此问题的范围。

答案 2 :(得分:0)

Unity有StopCoroutine()专门用于尽早结束协程。

您将要创建一个函数,当您要重置它们时,可以在每个技能按钮对象上调用该函数:

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