按下主页按钮或返回主菜单时取消协程
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
在不用的时候就处理掉,保证不会出现内存泄露。我建议在 MonoBehaviour
的 OnDestroy() 挂钩中这样做:
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 解决方案的细节,这可能会混淆答案并且超出了这个问题的范围。
我正在做的一些借口;我目前通过在协程中设置 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
在不用的时候就处理掉,保证不会出现内存泄露。我建议在 MonoBehaviour
的 OnDestroy() 挂钩中这样做:
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 解决方案的细节,这可能会混淆答案并且超出了这个问题的范围。