some pretext of what I am doing ; I am currently locking down my skill buttons via setting interactable = false in coroutines. Showing text of remaning seconds via textmeshpro and setting them deactive when countdown is over. But I am having problem when home button is pressed/ returned main menu. I would like to refresh my buttons cooldowns and stop coroutines when its pressed. But it is staying in lock position.
this is my cooldown coroutine
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);
}
this is where I catch when home button pressed/returned main menu. when catch it and pop 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");
});
...
I also stop time when game on the pause
public void PauseGame() {
Time.timeScale = 0.0001f;
}
You are using CancellationToken
incorrectly in this case. CancellationToken
is a struct that wraps a CancellationTokenSource
like this:
public bool IsCancellationRequested
{
get
{
return source != null && source.IsCancellationRequested;
}
}
Because it's a struct, it gets passed around by value, meaning the one you store in your list is not the same instance as the one that your Coroutine
has.
The typical way to handle cancellation is to create a CancellationTokenSource and pass its Token
around. Whenever you want to cancel it, you simply call the .Cancel()
method on the CancellationTokenSource
. The reason for it being this way is so that the CancellationToken
can only be cancelled through the 'source' reference and not by consumers of the token.
In your case, you are creating a token with no source at all so I would suggest making the following changes:
First of all, change your cancelTokens
list to be a:
List<CancellationTokenSource>
Next, change your OnButtonClick()
method to look like this:
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);
}
And lastly, change your cancelAllCoroutines()
method to this:
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();
}
I would suggest reading the docs on Cancellation Tokens or alternatively, as @JLum suggested, use the StopCoroutine method that Unity provides.
EDIT:
I forgot to mention that it is recommended that CancallationTokenSources
be disposed of when no longer in use so as to ensure no memory leaks occur. I would recommend doing this in an OnDestroy() hook for your MonoBehaviour
like so:
private void OnDestroy()
{
foreach(var source in cancelTokens)
{
source.Dispose();
}
}
EDIT 2:
As @Jack Mariani mentioned in his answer, multiple CancellationTokenSources
is overkill in this case. All it would really allow you to do is have more fine-grained control over which Coroutine
gets cancelled. In this case, you are cancelling them all in one go, so yeah, an optimisation would be to only create one of them. There are multiple optimisations that could be made here, but they are beyond the scope of this question. I did not include them because I felt like it would bloat this answer out more than necessary.
However, I would argue his point about CancellationToken
being 'mostly intended for Task'. Pulled straight from the first couple of lines in the 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
are lightweight objects. For the most part, they are just simple Structs
that reference a CancellationTokenSource
. The 'overhead' that is mentioned in his answer is negligible and, in my eyes, totally worth it when considering readability and intention.
You could pass a load of booleans
around with indices or subscribe to events using string
literals and those approaches would work.
But at what cost? Confusing and difficult-to-read code? I would say not worth it.
The choice is ultimately yours though.