Hello. I’m fairly new to asynchronous programming in Unity (since I used to never be able to get it to work lol) and I’m experiencing a really weird problem. Note that this is mainly to do with C# and not as much with Unity.
My game revolves around a central turn system and is largely event-based; however, I sometimes need the turn system to pause to allow other things to run before turns/phases are advanced. Because of this, I implemented an interrupter system that allows you to add an interrupter at any point, and the next time the turn system can pause, it will wait for the task(s) to complete. Each interrupter is in the form of a Func<Task> and they are all to be run simultaneously so long as they are in the list at the same time. These are then awaited, and once completed, the interrupters are cleared and the turn system continues from where it left off.
The problem arises with the function I want to add to the list: a health popup text (e.g. entity takes damage, text floats up from it showing the dealt amount). Yet, for some reason, when I try to run this, the function that should await the popup function instead skips over it, not even reaching the first line of the popup function to call a simple debug log. However, it does work for Task.Delay(), running perfectly as intended.
Do, however, note that the popup’s function works perfectly fine in normal applications. In an asynchronous loop run on Start, the calls can be awaited and run perfectly fine. It can also be run at a rate at which it runs multiple instances simultaneously with no problems at all.
So basically, I’m completely clueless as of now. I’ve been trying to figure this out for a while, but nothing seems to change anything for the better. So if anyone has even the slightest inclination of what might be happening, that would be greatly appreciated.
This first segment of code is the implementation of the interrupter awaiting. It mainly consists of a local async Task function wrapped inside a static IEnumerator that awaits it so that my turn system’s coroutines can await them at any point by simply using yield return AwaitInterruptersCompleted. I’ve also been messing with how the function handles the Task list, but nothing seems to be changing.
(Also, yes, I know there are a lot of debug logs in these snippets lmao, I think I’m going insane)
private static readonly List<Func<Task>> temporaryInterrupters = new();
private static readonly List<Func<Task>> persistentInterrupters = new();
public static void AddInterrupter(Func<Task> interrupter, bool persistent = false) {
if (persistent) {
persistentInterrupters.Add(interrupter);
}
else {
temporaryInterrupters.Add(interrupter);
}
}
public static void RemovePersistentInterrupter(Func<Task> interrupter) {
if (!persistentInterrupters.Contains(interrupter)) {
return;
}
persistentInterrupters.Remove(interrupter);
}
public static bool awaitingInterrupters { get; private set; }
private static IEnumerator AwaitInterruptersCompleted() {
Task task = Task.Run(AwaitInterrupters);
while (!task.IsCompleted) {
yield return null;
}
Debug.Log($"maybe done");
async Task AwaitInterrupters() {
if (temporaryInterrupters.Count == 0 && persistentInterrupters.Count == 0) {
Debug.Log("none found");
return;
}
Debug.Log("running...");
awaitingInterrupters = true;
List<Task> tasks = new();
temporaryInterrupters.AddRange(persistentInterrupters);
Debug.Log("running2...");
foreach (var interrupter in temporaryInterrupters) {
if(interrupter == null) {
Debug.Log("is null");
continue;
}
Debug.Log("wha");
tasks.Add(interrupter());
Debug.Log($"{interrupter.Method.Name}");
}
Debug.Log($"running3...");
await Task.WhenAll(tasks);
Debug.Log("running4...");
temporaryInterrupters.Clear();
awaitingInterrupters = false;
Debug.Log("really done");
}
}
The following is where the task is added to the list (this is inside the Entity script). I tried adding Task.Delay, and every time, it seemed to work perfectly fine. I did notice that it seems to be a regular Task function rather than async Task, but I still have no idea if or why that would break it.
protected virtual void Awake() {
health.Initialize();
health.onTakeDamage.AddListener(damage => {
Debug.Log("HELLOOO?");
//TurnSystem.AddInterrupter(() => Task.Delay(1000)); this runs perfectly fine
TurnSystem.AddInterrupter(() => HealthPopupSpawner.TrySpawnDamageText(damage, (Vector2) transform.position));
});
//...
}
The following is from the ScriptableObject HealthPopupSpawner. This class uses a singleton to store one set of fields, although I don’t see that being the problem since the TrySpawnDamageText function seemingly doesn’t even reach the first line. The IEnumerators following it seem unrelated to the problem, but I added them anyway, just in case.
public static async Task TrySpawnDamageText(float amount, Vector2 position) {
Debug.Log("running it"); // this doesn't even run despite the function supposedly being called?
#if UNITY_EDITOR
if (exitingPlayMode) {
return;
}
#endif
Debug.Log("running it1");
TextMeshProUGUI text = Instantiate(instance.textPopupPrefab, Level.current.worldCanvas.transform);
Debug.Log("running it2");
activeText.Add(text);
Debug.Log("running it3");
text.text = $"-{amount:#,0.#}";
text.color = instance.damageTextColor;
Debug.Log("running it4");
float startTime = Time.time;
Debug.Log("running it5");
IEnumerator movement = TextMovement(text, startTime, position);
movement.MoveNext();
Debug.Log("running it6");
IEnumerator fading = TextFading(text, startTime);
fading.MoveNext();
Debug.Log("running it7");
IEnumerator scaling = TextScaling(text, startTime);
scaling.MoveNext();
Debug.Log("running it8");
while (true) {
if (text == null) {
return;
}
Debug.Log("running it9");
float timeElapsed = Time.time - startTime;
Debug.Log("running it10");
movement.MoveNext();
Debug.Log("running it11");
fading.MoveNext();
Debug.Log("running it12");
scaling.MoveNext();
Debug.Log("running it13");
if (timeElapsed > instance.textLifespan) {
break;
}
await Task.Yield();
Debug.Log("running it14");
}
Debug.Log("running it15");
if (text == null) {
return;
}
Debug.Log("running it16");
Destroy(text.gameObject);
Debug.Log("running it17");
}
private static IEnumerator TextMovement(TextMeshProUGUI text, float startTime, Vector2 origin) {
Vector2 startPos = origin;
startPos.x += Random.Range(-instance.xVariance, instance.xVariance);
startPos.y += Random.Range(-instance.yVariance, instance.yVariance);
float velocityAngle = Random.Range(-instance.maxVelocityAngle, instance.maxVelocityAngle);
Vector2 velocity = Quaternion.AngleAxis(velocityAngle, Vector3.forward) * Vector2.up;
yield return null;
while (true) {
float timeElapsed = Time.time - startTime;
text.transform.position = startPos + (velocity * (timeElapsed / instance.textLifespan));
yield return null;
}
}
private static IEnumerator TextFading(TextMeshProUGUI text, float startTime) {
yield return null;
while (true) {
float timeElapsed = Time.time - startTime;
if (timeElapsed > instance.textFadePoint) {
text.color = new(
r: instance.damageTextColor.r,
g: instance.damageTextColor.g,
b: instance.damageTextColor.b,
a: 1 - ((timeElapsed - instance.textFadePoint) / (instance.textLifespan - instance.textFadePoint))
);
}
yield return null;
}
}
private static IEnumerator TextScaling(TextMeshProUGUI text, float startTime) {
Vector2 originalScale = text.transform.localScale;
text.transform.localScale = instance.startScale;
yield return null;
while (true) {
float timeElapsed = Time.time - startTime;
if (timeElapsed < instance.regularScalePoint) {
text.transform.localScale = instance.startScale + ((originalScale - instance.startScale) * (timeElapsed / instance.regularScalePoint));
}
else {
text.transform.localScale = originalScale;
}
yield return null;
}
}