#Understanding Async

1 messages · Page 1 of 1 (latest)

burnt karma
#

Hello, I'm currently struggling to understand how to implement Async for the following actions of my game:

  • Toggling between states in the animator
  • Loading scriptable objects and unmuting specific audio and dialogue tracks

I am aware I can do this with coroutines, but am really struggling to wrap my head around how to implement this with async.

For context, my game is short (~20 minutes) and is mainly interactive cutscenes, where you have to choose the appropriate dialogue. Additionally if the player gives a certain input, they are punished with a different set of dialogue. The sets of dialogue are controlled by two different timelines, so was wondering how you can implement something like having a timeline signal pause a timeline (yielding its coroutine), but also having an async respond to this input by changing the animator to a different state.

Hope this is clear, will appreciate any and all help!

burnt osprey
#

This demo uses a CancellationTokenSource to let us ensure only one execution of Animate() is running at once.
It doesn't have to be done this way, the async function can take a CancellationToken token argument instead to allow the caller to cancel it they wish.

burnt karma
#

I just realised as well that since the animations can be cancelled at any frame by the input, would it make sense to have that within a coroutine, but handle the dialogue options by async?

#

Like I'm having the speech animations loop essentially until they need to be changed

#

I'll defo peep that example tho

burnt osprey
#

Up to you but as I said before, you can make it all be async and fully replace Coroutines

burnt karma
#

So it's just bad practice to mix the two?

burnt osprey
#

but you can use both together, UniTask has some things to even let you await a coroutine!

#

I fully avoid coroutine now

#

You can still use both together but if you want to await an async function in a coroutine then it will require some jank

#
bool isDone = false;
MyAsyncFunc().ContinueWith(() => isDone = true).Forget();

while(!isDone)
{
   yield return null;
}
#

🤮

#

Or you would need to execute another coroutine via ContinueWith()

burnt karma
#

I was just trying to keep them separate by the input of the player essentially toggling a boolean that the coroutines would separately react to.

For example I have a progress bar that when filled fires an event to a central class, GameState, and this class relays that event to the separate classes that would have these coroutines. All the event does is change a boolean value essentially, that the coroutines can reference to control yielding.

For the async I was hoping I could tie this to a timeline signal where by the end of the dialogue chunk, the async reacts to this by playing an idle animation while waiting for a button press on the dialogue option menu, or until the player runs out of time of picking one. (I was gonna represent each of the options as integers.)

This integer is then used to load the next chunk of dialogue via string concatenation and file path, because my game is pretty small as it's just for uni.

burnt osprey
#

Well using a task completion source lets you cleanly wait for a button press so I would recommend this (I use this for this)

#

You could perhaps combine an event with async to let these things wait for some signal in a better way 🤔

burnt karma
#

Task completion was another thing I had trouble wrapping my head around

burnt osprey
#
await WaitForSignal("foobar");

...

async UniTask WaitForSignal(string signalName)
{
    UniTaskCompletionSource uniTaskCompletionSource = new();
    Action<string> onSignal = (string name) =>
    {
        if (name == signalName) uniTaskCompletionSource.TrySetResult();
    };
    gameState.OnSignal += onSignal;
    await uniTaskCompletionSource.Task;
    gameState.OnSignal -= onSignal;
}
burnt karma
#

is TrySetResult() built-in, idk if this is a dumb question

burnt osprey
#

the concept is the same across all of these "completion sources", it lets you await a non async thing such as an event.

burnt karma
#

Ah ok cool, I think what I'll do is that for now I'll do everything with coroutines but try to at least get the dialogue options to work with async, that seems like the most simple implementation of async in my game for now

burnt osprey
#

Yea you are free to do as you wish. Ill point out once more that you can "check each frame for some bool" with coroutines or async.

#

I am very comfortable with async so i use completion sources a lot

burnt karma
#

Thanks, it's mainly because this project is due in september, and I still need to implement eye tracking with tobii eye tracker 5, I'm literally just using it as an input with Unity's raycast system. Just been doing it off of mouse hover for now

Pretty much conversion courses in the UK are a means of retraining when you have little prior knowledge

burnt osprey
#

hmm not heard of those as a brit myself

burnt karma
#

They're pretty much one-two year intensive postgrad courses with the idea of prepping you for a field similar to undergrad, it's a means of career change pretty much.

#

They have them for a lot of areas like law, psychology, marketing etc.

burnt osprey
#

experience is very important in this industry so having good portolio pieces will matter a lot. After you get some professional experience no one will care about education stuff anymore

burnt karma
#

True, I only did this degree to help me check a box, after I finish this dissertation I'm thinking of expanding my project idea into something bigger and building more of a portfolio with Unity projects.

#

But for now, just need to finish a minimum viable product and get people to test it.

burnt osprey
#

I wish you luck then, ask if you have any async related questions

burnt karma
#

I will, at the moment just getting all the mechanics sorted separately before putting it all together, I might need a little bit of help. Like at the moment just understanding how to use async with the dialogue options, if you don't mind I'll probably post code here if I get stuck. After I get this sorted, then it's sorting out the tracker with Raycast with Tobii's API, then we're basically done and just have to put it all together.

Figuring out how to actually use the individual components first is the hard part anyway

burnt karma
#

Ok so I had a go at understanding the github document you sent me, the animation one, this is what I've gathered from it.

  • You've initialised a CancellationTokenSource as a global variable, then in start you call AnimateExample.Forget(). Forget() is used to ignore task completion on the initial call.

My guess is that this was done so that because start is not an async function, with forget we can call AnimateExample without throwing an error or warning.

And we don't want start to have to wait on initialising the animation.

  • In AnimateExample(), we first start by yielding a task and awaiting for Animate().

This works by first calling CancelAnimation() to make sure that we don't have a stack of cancellation token's, preserving memory.

After this call we assign value to the animateTokenSource object with CreateLinkedTokenSource, meaning animateTokenSource acts as a pointer to this, with destroyCancellationToken acting as a parameter.

We then extract the cancellation token itself from animateTokenSource, using animateTokenSource.Token.

We then initialise the starting position as the current position of the object, that this script is attached to. As well as a float value that starts at 0, where from the equation in the while loops with Time.deltaTime, it kinda acts as a percentage of how much of the animation is done.

  • Then in the while loop, this continues until either the Cancellation of Animate() is requested via a token - possibly from another method or class? Or once lerp's value no longer fits the inequality, I'm guessing that the 0 is a typo and meant to be a 1, otherwise this while loop would never run?

This while loop runs frame by frame, passing the cancellation token between each frame.

  • After this while loop, we then have one last if statement that checks whether the async was cancelled via the token.

  • Finally, the animation is run again with different parameters after checking that the script wasn't destroyed?

#

Also I'm not entirely sure about the purpose of the second forget, my guess is that that was done to demonstrate the difference between using forget to bypass awaits in an async and not?

@burnt osprey

burnt osprey
#

It was to demonstrate how the second Animate() call will cancel the one before it so both dont execute and clobber eachother

#

.Forget() is something UniTask provide to make sure un awaited tasks report exceptions

#

If not used, they only get printed when you stop playing

burnt karma
#

Oh ok so you ran it at first to make sure that the actual call you're overriding with did not throw any exceptions? So it's like a try catch in that way?

Sorry for the @, I really appreciate the help

#

What about the rest of what I said, is my understanding there?

burnt osprey
#

We do var token = animateTokenSource.Token; to make sure we can refer to the correct token source instance and check if THAT was cancelled.
CancellationToken is a struct so is copied. This means when we cancel animateTokenSource and replace it with a new instance, that past execution can still stop itself.

#

You are correct, that loop condition is a typo 😆 , it should be while < 1f

hearty torrent
#

A couple of notes:
An alternative to Forget if you're not using UniTask is to use async void for fire-and-forget. https://unity.huh.how/programming/async/exceptions
It's probably also with reading my description of why.

The destroyCancellationToken is an important thing to understand, it's a MonoBehaviour property that is cancelled when the object is destroyed.
If you didn't use it, then your task will leak into the editor when exiting play mode, (or if your object is destroyed, it will continue).
Async methods don't have built-in behind the scenes lifecycle controls like coroutines do.

burnt osprey
#

Correct, I make a token source that is "linked" to this so it is cancelled when the mono is destroyed:
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);

#

The alternative is checking this == null

hearty torrent
#

Most of the time I've got no MonoBehaviour, so there's a huge stack of async methods with cancellation token parameters and linked tokens where inner loops need their own cancellation support

crystal nest
#

It also helps to understand Tasks and async/await on a higher level rather than focusing on specific code examples:

  • Programming has many asynchronous operations. By asynchronous I don't mean the keyword async, but rather anything that takes some time to complete rather than immediately, and the caller wants to be notified when it's completed/cancelled/failed.
  • Task is an abstraction over asynchronous operations. This abstraction allows a uniformed interface to work with asynchronous operations.
  • However not all code in the world are written with Task, so if you want to use Task with some code that isn't, then you need a way to turn it into a Task. The way to do that is TaskCompletionSource.
  • Cancellation is a mechanism to notify the task to cancel early when the caller doesn't care about it anymore.
  • And finally, async/await is just syntax sugar to make your callback hell code into "normal" pretty looking code.
    It really helps to understand why these exist instead of just how to use them. Try think of concrete code examples for each of these points.
burnt osprey
#

JS promises perform the same role for async there, lots of older code is riddled with lots of .then() s

crystal nest
#

Yep, JS has pretty much exactly the same API surface as C#:

JS              C#
Promise         Task
Promise#then    Task#ContinueWith
Promise.all     Task.WhenAll
new Promise()   TaskCompletionSource
AbortController CancellationTokenSource
AbortSignal     CancellationToken

And of course async/await in both languages turn callback hell code into pretty code just the same way.

crystal nest
#

As for practical advice regarding this in Unity, personally I'm of the opinion that you either pick coroutine or async/await, and stick to the one you pick for the entirety of your project instead of mixing them.
(Coroutine is also an abstraction over asynchronous operations, just a different one, and one that is not used outside of Unity)
Personally I pick async/await, and you can use whatever asynchronous abstraction you like. The Task from C# BCL is one option, but Unity also has Awaitable nowadays, and UniTask is a third party library that's been around forever. I personally use UniTask.

burnt karma
#

Ok so I tried installing the UniTask package and I got this error?

I'm pretty sure it's the right URL and I'm using Unity 6

burnt osprey
#

Do you have git installed?

hearty torrent
#

It's a much nicer experience if you add it via openupm, not sure if those instructions are there (you shouldn't have to download a tool or anything)

rotund niche
rotund niche
# burnt osprey I fully avoid coroutine now

in the case of regular c# Tasks, coroutine is useful for frame dispatching simply bcos Tasks will be out of sync with Unity's PlayerLoop.

You can in a poorman's way dispatch your continuation that still respects the player loop with coroutine

yield return WaitNextFrame();

while(asyncPool.TryDequeue(out var queuedAsyncContinuation))

burnt osprey
#

i spoke in reference to how its used and i think thats all that matters here

rotund niche
#

Avoid TaskCompletionSource<T> in regular Tasks if they're executed every frame.
instead make a custom class that implements INotifyCompletion OR INotifyCompletionCritical then reuse them over and over (you can reset then state however you want) or just let them be a struct

#

the docs about INotifyCompletionCritical is a bit non-existent, but you can check the dotnet source if you want to learn about them

#

either way, the non-critical version should be enough

burnt karma
#

Ok ngl, I'm still really stuck on understanding TaskCompletionSource, any of you lot know some examples to look at?

I really appreciate the help.

#

Like I understand the animation example now, but TaskCompletionSource is a whole nother thing.

burnt osprey
# burnt karma Like I understand the animation example now, but TaskCompletionSource is a whole...

the task completion source objects exist to let us await some arbitary event/function

UniTaskCompletionSource completionSource = new();

Action clickCallback = () => completionSource.TrySetResult();
button.onClick.AddListener(clickCallback);
await completionSource.Task;
button.onClick.RemoveListener(clickCallback);

In the simple example above, we make a new completion source instance and subscribe to a button click event. this subscription will tell the source to complete.
We then await the Task from the completion source, this awaited task completes when the button is clicked.

The end result is we can await a button press in async code without a hacky check each frame/period of time.

#

The example can be improved by cancelling or completing the task when the button is destroyed too otherwise this could wait forever post play mode

burnt karma
#

UniTaskCompletionSource completionSource = new();

Action clickCallback = () => completionSource.TrySetResult();
button.onClick.AddListener(clickCallback);
await completionSource.Task;
button.onClick.RemoveListener(clickCallback);

I can achieve this with 'regular' TaskCompletionSource as well right?

Pretty much what I'm trying to do is that when the timeline reaches a certain part of the game, the dialogue option menu opens up. So with the dialogue options, could I have the buttons on the menu send a signal to this TaskCompletionSource and then assign it value with TrySetResult?

#

Like I understand that action here is unity events, but really not sure how that works out with clickable buttons if you get me

burnt osprey
#

Yea, the unitask version or awaitable version has some improvements for Unity so you should use these instead

burnt karma
#

Ah ok then, at least the fundamentals of trying to understand the task stuff will carry over.

#

That's literally all I want to implement the async stuff in for now, then when I'm more comfortable and if I have time, rework the coroutines into async

burnt osprey
#

My example can be turned in to a function for example that you can use with ANY button:

public static async UniTask AwaitClick(this Button button)
{
    if (button == null) return;

    UniTaskCompletionSource completionSource = new();

    Action clickCallback = () => completionSource.TrySetResult();
    button.GetCancellationTokenOnDestroy().Register(() => completionSource.TrySetCanceled());

    button.onClick.AddListener(clickCallback);
    await completionSource.Task;
    button.onClick.RemoveListener(clickCallback);
}
#

How you use this can be applied to many areas. when you want to wait for the button click, you set this up and then await it...

#

UniTask may already have button click async extensions so if so use those

#

(ugui button)

#

unitask is just better than awaitable it has way more useful features

burnt karma
#

Alright then cool, just installing git now too.

What I tried doing at first was have the button call a function which then completes the task with TrySetResult and assigning an enum value.

Then with the enum value, use a switch statement to convert it to an integer, that then gets passed along a unity event to other relevant classes.

I had the cancellation token just be a timer value, where the progress bar associated with the timer value ticks down every frame so if it runs out I just pass along a 0 instead.

I was a bit confused on what to do with the cancellation token after tho

#

Although why would git being installed do anything if I'm installing the package via URL?

burnt osprey
#

what you just explains sounds weirdly complex and wtf are you doing with the cancellation token 😐

#

git is needed to install the package via git, otherwise use the open upm version

burnt karma
burnt karma
burnt karma
#

I'm really confused as to why I'm getting these errors on my code, like I'm pretty sure how I've approached it algorithmically is fine, it's just I'm really drawing a blank on the syntax.

#

Like I don't get these errors when using it with async tasks instead of UniTask.

#

Also when reading the documentation for APIs, do you guys have any general advice on where to start, asking because a lot of times there's so much that it can be a little overwhelming on what to understand first if you get me?

#

Pretty much when the dialogue options menu is supposed to open, it fires an event to my GameState class which then relays this event to all other classes that need to respond to it, such as one for animations and one that controls my timelines. Treating gamestate as a hub so that they only need to reference gamestate instead of tons of other scripts init.

Also I realised I forgot to put the cancellation token in my yield statement for the UniTask version, will I need this for task.

crystal nest
burnt karma
#

So for the code I've done for Task, it doesn't show up in either for now, although I haven't play tested tbf, as I haven't assigned the objects yet - they're in the editor hierarchy but I got really fixated on this part.

But with the UniTask version, I get errors in both the console and IDE. It's mainly to do with this snippet of code in the UniTask version:

#

If it helps provide context, my game is basically interactive cutscenes where because it's small scale and I want to make sure the dialogue and audio are perfectly sync, that it's handled with timeline where markers on the audio tracks dictate whether going to the next line or opening up this dialogue menu.

Thank you so much, I really hope how I've been replying doesn't feel like a waste of your time.

burnt karma
# crystal nest Does the error show up in Unity console, or only in your IDE?

Also just to ask, does my code look ok for the task one to you, not the UniTask one?

Like I have used Deepseek to help but not to copy and paste code from it, instead I'll ask it questions about concepts and assess whether my approach to something will work. Like with this while I could do it by TaskCompletionSource, because I also need to pass an integer via a unity event, I ended up doing it this way because I wasn't sure how else to do it and knew how to return values of Task<T> functions.

crystal nest
crystal nest
burnt karma
burnt karma
crystal nest
#

Unity console is the important one. If you only see error in IDE but not in Unity, then that could mean it's just an issue on the IDE part and might be fixed by regenerating projects. But if the error is in Unity console, then that means likely your UniTask installation is just messed up somehow.

burnt karma
#

Ah ok, got it, thanks anyway man, like I know I could probably do this with Coroutines, but for dissertations polish does help and might as well learn it anyway to improve

crystal nest
#

Yeah coroutine in Unity is just a caveman version of async/await. The code in screenshots above still have some coroutine tendencies.

burnt karma
#

Fairs, I only wanted to check frame for frame because of the timer

crystal nest
#

Yeah instead of writing a manual frame by frame loop like that to check the time, the idiomatic async/await way to do it is to just create a cancellation token that cancels after a certain amount of time, and pass that to the task.

burnt karma
#

Even if I wanted to have the timer be represented visually?

crystal nest
#

I suppose it's acceptable depending on how much you want to decouple the two things (the task and the presentation of the choosing UI)

burnt osprey
#

If you want to wait for something with a timeout, perhaps the async func can do both, wait for a result and have a delay that stops it early. You can use something like UniTask.WhenAny() to wait for a button click OR a UniTask.Delay() and then either return the result or another value to indicate it was cancelled.

#

using a cancellation token is still a good option as long as its handled correctly

#

Do note that if passed to a function such as UniTask.Yield() it will throw an exception unless you do:
await UniTask.Yield(cancellationToken: token).SupressCancellationThrow();

limpid musk
#

you aren't going to cancel any tasks, so you don't need any cancellation tokens

#

it's not meant for modeling interruptible cutscenes.

limpid musk
#

that's why people like it

#

your callstack looks sanes

burnt karma
# crystal nest Can you show the code in text?

using UnityEngine;
using System;
using System.Threading.Tasks;
using UnityEngine.EventSystems;
using System.Threading;
using UnityEngine.UI;
public class optionsMenu : MonoBehaviour
{
public GameObject optionsMenuPrefab;
public GameObject dialogueMenuPrefab;
public Action<int> optionSelected;
public GameState gameState;
public Image timerBar;

private CancellationTokenSource optionCTS;
[SerializeField] private int decisionTimer = 5;

[Header("First selected options")]
[SerializeField] private GameObject optionsMenuFirst;

private int optionNumber = 0;
private bool madeDecision = false;

private void Awake()
{
    gameState.decisionTime += chooseOption;
    optionsMenuPrefab.SetActive(false);
    dialogueMenuPrefab.SetActive(true);

}
void flipMenus()
{ if (!optionsMenuPrefab.activeInHierarchy)
    {
        optionsMenuPrefab.SetActive(true);
        dialogueMenuPrefab.SetActive(false);
        EventSystem.current.SetSelectedGameObject(optionsMenuFirst);
        timerBar.fillAmount = 1f;
        optionNumber = 0;
        madeDecision = false;
    }
    else
    {
        optionsMenuPrefab.SetActive(false);
        dialogueMenuPrefab.SetActive(true);
        EventSystem.current.SetSelectedGameObject(null);
    }

}
private async void chooseOption()
{
    refreshToken();

    optionCTS = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
    CancellationToken optionToken = optionCTS.Token;

    flipMenus();
    int choiceNumber = await DecidingTime(optionToken);

    sendDecision(choiceNumber);
    refreshToken();
}
#

private async Task<int> DecidingTime(CancellationToken optionToken)
{

    while (!madeDecision && timerBar.fillAmount > 0f && !optionToken.IsCancellationRequested)
    {

        timerBar.fillAmount -= (Time.deltaTime / decisionTimer);

        await Task.Yield();
    }

    if (timerBar.fillAmount <= 0f || optionToken.IsCancellationRequested)
    {
        optionNumber = 0;

    }

    return optionNumber;
}
void setOptionValue(int optionNum)
{
    optionNumber = optionNum;
    madeDecision = true;

}   
private void refreshToken()
{
    if(optionCTS != null)
    {
        optionCTS.Cancel();
        optionCTS.Dispose();
        optionCTS = null;
    }
}

void sendDecision(int decisionNum)
{
    optionSelected?.Invoke(decisionNum);
    flipMenus();
}
void endGame()
{
    gameState.decisionTime -= () => chooseOption();
    optionCTS.Cancel();
    optionCTS.Dispose();
}

}

Ok so this is the code I had in the screenshots with using task

#

Pretty much only reason I wanted to check every frame was so I could have a visual progress bar tick down, to give the player a time limit.

If I really don't need async, then might as well stick with coroutines I guess for now?

crystal nest
#

(You should still figure out what's wrong with your UniTask installation though)

burnt karma
#

My main thing with tasks is I have trouble understanding how to think about the tokens tbh, like if you dispose of the source, it cancels all the tasks that use the token of that source right?

crystal nest
#

No, disposing doesn't trigger a cancel.

crystal nest
# burnt karma private async Task<int> DecidingTime(CancellationToken optionToken) { ...

I don't have much to comment on, except that instead of checking optionToken.IsCancellationRequested, you should just pass that token to the next thing you await. If you are using UniTask for example, you can just do await UniTask.NextFrame(optionToken) and that will automatically check cancellation for you. That's the general idea behind cooperative cancellation: you pass the token down to everything you await, and let them do the cancelling themselves, instead of you checking after every await.

#

Other than that, I'm under the impression that your whole game's code is based around events, so if you want to ask player for a choice you would do something like "instantiate/get a reference to the option menu, attach an event handler to the optionSelected event to receive the result, then call a method on the menu to start the process."

#

Reminder that what an asynchronous operation is, it is anything that the caller starts, but takes some time to complete, and the caller wants to know the result. That whole process of asking player for a choice is exactly an asynchronous operation, and you can neatly wrap it into a task.
For example:

UniTask<int> Ask()
{
    var utcs = new UniTaskCompletionSource<int>();

    var optionsMenu = Instantiate(...);
    optionsMenu.optionSelected += result => utcs.TrySetResult(result);
    optionsMenu.Show();

    return utcs.Task;
}

And now you can simply just call and await it, any time you want to ask player to choose something:

var result = await Ask();
// handle result however you want
#

You have the power to structure your entire game like this instead of a convoluted ball of events and handlers.

burnt karma
# crystal nest Reminder that what an asynchronous operation is, it is anything that the caller ...

So what about the cancellation token for this then? Or is that to be handled in another function?

I had no clue you could subscribe events from a class to events in the same class tho. So I could just have the buttons on the menu object call option selected, and have the integer tied to try set result here with delegates?

I don't know how you would extract the integer from this tho, thanks so much.

#

Also I'm sorry if this is annoying, I know I'm doing a dissertation but it's for a conversion course so it's quite different to a traditional CS degree

crystal nest
#

If I was to go all in on the async/await instead of events, I wouldn't even have the optionSelected at all. The optionsMenu can just hold onto a UniTaskCompletionSource and let the buttons directly call utcs.TrySetResult on it.

#

The purpose of cancellation is to give someone else the ability to cancel a task. If you don't care about that, then you don't need it.

burnt karma
#

Ah ok, so I was tunnel visioning on that then, I thought you had to pass the token no matter what.

So when you say you would just hold onto the UniTaskCompletionSource, and let the buttons do so directly, I'm guessing this would be in a separate function to call TrySetResult. And then you can return the integer with utcs.Task?

crystal nest
#

utcs.TrySetResult(5) already passes the 5 back to the task, so the caller that awaits the task will get 5.

burnt karma
#

Like what I'm struggling to get is if you were to have the buttons call utcs.TrySetResult, I assume this would have to be in a separate function, but at the same time if utcs is inside Ask can they still access utcs because wouldn't it be local to Ask?

#

Or would you have utcs at the class level, and outside the ask task function?

crystal nest
#

Let's start with UniTask<int> Ask(), it's just a function that returns a UniTask<int>. That's the equivalent of "I've started the asking operation, it will be completed later with a result of int, you can track the operation using the returned task."
The caller of Ask() now gets a task, the caller can then await it. That's the equivalent of "I'm waiting on this operation to finish and want the resulting int."
The utcs.TrySetResult(5) will set the task's result to 5 and signal the operation's completion. That's the equivalent of "the operation is done, whoever is waiting on it, the result is 5."

burnt karma
#

So does this mean that you would still have TrySetResult inside Ask, then have another function first call it, then have that same function await to retrieve 5?

crystal nest
#

No, the TrySetResult can be called anywhere.

#

The purpose of TrySetResult is to say "the task is done and here's the result" so whatever is responsible for determining the task is done, should call it.

#

In the case of an option menu, the button of an option being clicked determines the task is done, so the button clicking code should call it.

burnt karma
#

oh ok, so you can still have the utcs initialisation inside Ask(). And then for the button's on click event, have it call a function that calls Ask to act as a sort of pointer to the task, then TrySetResults with the button's integer value, then await to complete the task?

crystal nest
#

Sure, or Ask can just give the utcs to the button so the button can call TrySetResult directly.

#

If you are all in on async/await, there's no point in having a middle man of button -> middle man -> utcs.TrySetResult.

burnt karma
#

I really appreciate your patience with me btw, thank you

crystal nest
#

No, Ask is the one acting as a bridge between the caller and the button.

#

Ask creates a UniTaskCompletionSource, and then:

  • Give the utcs.Task to caller, so that caller can await on the task.
  • Give the utcs to button, so that button can utcs.TrySetResult(...) to tell whoever waiting on the task that it's done.
burnt karma
#

So would the caller be what opens the menu to return the task, and then the on click event for the button just does utcs.TrySetResult?

I think I get the flow of execution here now, where the idea is that Ask() creates our utcs, then we can give the task to the caller. The button itself completes the task with TrySetResult(whatever integer passed to the button's on click).

Then we can await with caller to retrieve the integer?

crystal nest
#

Yes, the caller can decide what it wants to do with the task it gets.

#

Most commonly the caller will wait on the task to complete and use the result in some way. Sometimes the caller doesn't care and just ignore the task and go on.

burnt karma
#

So how would you pass the utcs to the button's on click function directly then?

Also I'm guessing with adding a timer, just have it in Ask as a cancellation?

crystal nest
#

You can pass it however you want, it's not any different from a regular variable.

#

For example, make a property and have Ask set that property.

#

For a timer that cancels the asking after some time, there are two ways you can approach it conceptually:

  • Ask never cancels itself, but allows the caller to cancel it by passing in a cancellation token.
  • Ask cancels itself, so it should handle its own cancelling without caller's involvement.
burnt karma
crystal nest
#

How would you pass something to a MB at runtime?

burnt karma
crystal nest
#

MonoBehavior

burnt karma
#

Oh god lmaooo, well you could store in a variable or pass to it from another script via event. Or just do a set method

#

So I could have the button's as a separate class, have ask() pass utcs to a method of this class, then with the on click for button have it TrySetResult?

#

Also I was wondering that if you TrySetResult for a task, can it be overriden with subsequent try set results for that task or is it more of a "one and done"

burnt karma
#

And just to check we're not doing something like

private UniTaskCompletionSource<int> utcs, outside of Ask()?

mellow pumice
#

TrySetResult should fail and return false if you already set a result, a task can only be completed once

burnt karma
#

Ok cool thanks, that makes stuff easier.

burnt osprey
#

Im going to re send my original completion source example because you need to make sure object destruction will cancel or also complete the task.
Why? If the task does not complete and you stop playing, whatever is awaiting this will still be "waiting" forever stuck.

public static async UniTask AwaitClick(this Button button)
{
    if (button == null) return;

    UniTaskCompletionSource completionSource = new();

    Action clickCallback = () => completionSource.TrySetResult();
    button.GetCancellationTokenOnDestroy().Register(() => completionSource.TrySetCanceled());

    button.onClick.AddListener(clickCallback);
    await completionSource.Task;
    button.onClick.RemoveListener(clickCallback);
}
#

make sure you keep destruction AND unsubscribing in mind where required

burnt karma
#

And like I don't know how I could map this to something like on click from the unity editor

burnt osprey
#

If you have a custom monobehaviour for this button that returns this value via an event you can easily wrap around THAT event instead

#

My example is for a UGUI button, when awaited it subscribes to the ugui button and waits until the button is clicked to complete

#

Its written as an extension method

burnt karma
#

So I'd make the buttons a separate class? Asking since you said custom monobehaviour

burnt osprey
#

I do this all the time, make a monobehaviour for ui objects
e.g. LevelButton that has an event public event Action<string> OnClick; that passes the level id as an arg.

#

then in the level button we can do:

public event Action<string> OnClick;
public string levelId;

[SerializeField]
Button button;

private void Awake()
{
  button.onClick.AddListerner(() => OnClick?.Invoke(levelId));
}
burnt karma
#

So I could the same sort of thing with my dialogue but just with an integer instead?

burnt osprey
#

Regardless the core information given to you about completion sources is the same

#

well yea its just a variable

#

I presume you instead want to await a few potential UI interactions and get back some variable that identifies which one?

burnt karma
#

Pretty much all I wanted to do was open the menu, await for which of the options is picked, then take the integer that corresponds to the option picked.

Pass the integer to other scripts via event, so I can do things like unmuting the correct audio and control tracks in the timeline as well as whether or not to punish the player by taking a life away if they chose the wrong option.

#

Decreasing lives would be from loading the dialogue option's scriptable object and checking whether it was the correct one through the boolean of the scriptable object.

burnt osprey
#

Okay that makes sense, you can then have something like:

private UniTaskCompletionSource<int> buttonClickCS = new();

for(int i = 0; i < buttons.Length; i++)
{
  var button = buttons[i];
  button.onClick.AddListener(() => buttonClickCS.TrySetResult(i));
}

public async UniTask<int> AwaitButtonClick()
{
  return await buttonClickCS.Task;
}
#

you may need to reset (set it to a new instance) buttonClickCS each AwaitButtonClick() call to make it re usable each time but its just a rough example

burnt karma
#

Then I'm guessing afterwards I'll need to destroy the button game objects then load new ones when ready? And to do that I can call a method in the other class that takes the buttons list and destroys the gameobjects in that list?

burnt osprey
#

What I wrote could be put in a parent class that controls some amount of buttons

You can have a chain of awaiting if you want yes (better to await instead of returning the task directly) but thats up to you

#

It could be made re usable doing something like this

private UniTaskCompletionSource<int> buttonClickCS;

for(int i = 0; i < buttons.Length; i++)
{
  var button = buttons[i];
  button.onClick.AddListener(() => buttonClickCS?.TrySetResult(i));
}

public async UniTask<int> AwaitButtonClick()
{
  if(buttonClickCS == null) buttonClickCS = new();

  int id = await buttonClickCS.Task;
  buttonClickCS = null;
  return id;
}
#

Its different to account for AwaitButtonClick() being called multiple times before a button click

burnt karma
#

Ah ok so in that case with this code, I wouldn't need to destroy the button objects as we're creating a new instance of the taskcompletion source each time, when we call the AwaitButtonClick method from another class that acts as our caller?

Sorry if this is frustrating, on our conversion course we didn't cover async programming, just OOP.

burnt osprey
#

Yes but Ill be honest I dont get why you thought the buttons need to be destroyed

burnt karma
#

I think I just horribly misread when you said something about resetting buttonClickCS

burnt osprey
#

This way we can do stuff like:

int optionA = await buttonUI.AwaitButtonClick();
int optionB = await buttonUI.AwaitButtonClick();
#

Yea by "reset" i mean make a new completion source instance as we know it can only "complete" once

burnt karma
#

Thanks a lot for your help btw, I did do some tutorials, checked out your example and some other examples on the Task API for C#, asked Deepseek as well (just on the concept, not to copy 'n' paste fuck that), but couldn't wrap my head around how the hell to actually implement it.

burnt osprey
#

as you do more you hopefully will get more comfortable with how async works

burnt karma
#

True, that's why for now I just want it to work with the dialogue options, either way I really appreciate the help man.

junior snow
#

Just a question here, why don't you emit an event on button click instead?

#

The event arguments could include the choice as an int and you can have listeners that individually handle the choice, either with async tasks or otherwise.

burnt karma
junior snow
#

You can then dispatch Tasks from there to change settings or otherwise control the game, but I wouldn't tie that to the UI.

#

It's also very likely bad news to alter state or send events or data across the different timelines / threads. It spells heaps of trouble down the line with debugging when/how the state is changing.

burnt karma
# junior snow I would structure the code so that the UI (which is stictly synchronous) only se...

If it helps, I only have two timelines at work, one to play the 'main dialogue' and one if the player does an input that would interrupt the NPC, playing a different sequence the player can't skip, it's purely as a punishment. But if the dialogue options are up, they're not able to interrupt, as the interruption is fired when one of two progress bars are filled, and these progress bar coroutines will be paused when the dialogue options menu is open.

#

I do have a separate class to handle the timelines already yes, these respond to the same signal that opens the dialogue options menu, as that timeline signal is fired to a class I have called GameState, which I'm just treating as a hub to relay events to other classes. So if the timeline fires to GameState, the class that handles timelines will receive this and just pause the 'main' timeline. Likewise with resuming the timeline once the integer is received

junior snow
#

So is the only challenge here that in some state the player can interrupt but in other states they can't?

burnt karma
#

Yeah pretty much, they can only interrupt in one of the timelines and not when the dialogue option menu is open. It's a dissertation so just going with a small prototype to user test with.

junior snow
#

Okay understood

#

So perhaps your GameState class can know about the state and receive events about interruptions, then allow or disallow (gracefully) and change state as needed

#

the timelines don't have to know about any of this stuff

burnt karma
#

Pretty much my project's on serious games so just wanted to make something simpler for now

#

But once I finish, really dig more into Unity's API, I'm converting from social sciences so if I did anything too technically intensive I'd be fucked haha

junior snow
#

This is already pretty involved so kudos UnityChanThumbsUp

#

and good luck 🙂

crystal nest
#

Callback type code (eg events) does not scale well in terms of complexity. If the project has a lot of asynchronous operations, you really don't want it to be a ball of interconnected convoluted events and handlers.

junior snow
#

I think if they were doing something larger yes but a dissertation project on a deadline is likely more taking to "if it works it works". But yeah generally you want to control the direction, sequence and temporality of events for testing purposes. But probably not too applicable right now.

burnt karma
crystal nest
#

Sure, not saying you have to use it for this project, just stating the advantages.

burnt karma
#

What you said did help me understand the flow of execution better

crystal nest
#

If you want a practical example of events failing to keep up with complexity:

void OnClickStart(int levelId)
{
    if (!HasSeenTutorial)
    {
        var askPlayTutorialDialog = Instantiate(...);
        askPlayTutorialDialog.OnSubmit += result => OnSubmitAskPlayTutorialDialog(result, levelId);
        askPlayTutorialDialog.Show();
    }
    else
    {
        StartLevel(levelId);
    }
}

void OnSubmitAskPlayTutorialDialog(bool result, int levelId)
{
    if (result)
    {
        var tutorial = Instantiate(...);
        tutorial.OnFinish = () => StartLevel(levelId);
        tutorial.Start();
    }
    else
    {
        StartLevel(levelId);
    }
}

void StartLevel(int levelId)
{
    // Actual logic for starting a level
}

The code is written in the typical callback style with events. OnClickStart is the entry point of the code when player clicks the start button of a level, what does it do?
If you have to scan the code back and forth and piece together the execution flow of the logic, then that's exactly the problem. On top of that, see that OnSubmitAskPlayTutorialDialog also has to carry information of previous step (in this case levelId) over, and the more step you have, the more information you have to carry around.
All of this code is just for a logic that you can describe in one sentence: "if player hasn't seen tutorial and choose yes when asked, play the tutorial, then continue to the level."
If you instead write this code in async/await, it's as simple as:

async UniTask OnClickStart(int levelId)
{
    if (!HasSeenTutorial && await AskPlayTutorial())
    {
        await PlayTutorial();
    }

    // Actual logic for starting a level
}
#

Not to say you must use async/await and never write callback based code, but now you have the knowledge you can make more intelligent decision on which one to choose in which situations.

burnt karma
#

yeah it makes sense, looks like what you did was see if AskPlayTutorial has actually completed, to then see if the tutorial is completed before jumping into the level selected by the UniTask itself first. It's a lot more compact.

rotund niche