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