Async/Await ... NavMeshAgent

Hey everyone,

im trying to work with Async/Await instead of Coroutines and i just setup a test project to try to make my AI path to a certain area and then on cancel path back and thats it.

So everything does work super! BUT in case i want to work with CancellationTokens, it does not, i get a few errors as “X Function can only be called on main thread”, ok i do know what it means but how can i run my function on the main thread as it did before?

Working example:

    public async void Interact(GameObject entity)
    {
        if (InteractingUnit) return;

        InteractingUnit = entity;
        if (entity.CompareTag("Player"))
        {
            await InteractWithPlayer();
        }
        else
        {
            print("Interacting with NonPlayer");
        }
    }

    public async Task InteractWithPlayer()
    {
        NavMeshAgent agent = InteractingUnit.GetComponent<NavMeshAgent>();
        agent.SetDestination(MovePoint.transform.position);

        print("Calculating Path");

        while (agent.pathPending)
            await Task.Yield();

        print("Moving");

        while (agent.remainingDistance > 1f)
            await Task.Yield();

        print("Arrived");
    }

and since i want to work with cancellation tokens, that doesnt work cause it throws the above error in any cases

            Task.Run(() => InteractWithPlayer(), _cts.Token);

only by adding Task.Run with the cancellationToken i cannot run this anymore, since i did never work in unity with async/await i would love to hear any solution on this, since google couldnt help

Small Add aside, the NavMeshAgent in the title is wrong since it does throw the error in any case of GetComponent or trying to access any Getter

Correct. Unity API calls must be made from the main thread.

If you review the docs for Task.Run():

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=net-6.0

You will see the statement:

Queues the specified work to run on the thread pool and returns a Task object that represents that work.

To me that means it will run on something besides the main thread.

As an aside, one of the beautiful things about coroutines is that they die quietly when the next scene loads, or when objects get destroyed. This means coroutines run on a given object that only deal with stuff on that object (or its children) all quietly go away, no muss, no fuss.

Coroutines are almost magical really in how simple they are. Dying peacefully when the MonoBehaviour running them dies is perhaps one of their most incredibly valuable properties, besides the rock-solid reliability of being cooperatively tasked.

If you want to use this newfangled async/await stuff, beware that you need to maintain 100% awareness of GameObjects that might go away while still being referred to by ongoing tasks. If this is the case, you will get Missing or Destroyed object errors from within those tasks, even when they are correctly run on the main thread.

Yes, but here is the problem,
in my case i need to be able to check if the Coroutine is still running and be able to Stop it at any time,
i know i can access the iterator of the Coroutine and check if “HasNext()” returns true or false, but that imply the need of a wrapper class which i do want to omit if its possible,

async/await looks quite nice to me, to worry about, in multiple cases where i need to make error handling, thats no problem if done correctly

So if i cache the “Function” in a “Task” variable, and call after that the “Task.Run” function it does work, but why do i have todo that?

It feels like not the best Practise

    public async void Interact(GameObject entity)
    {
        if (InteractingUnit) return;

        InteractingUnit = entity;
        if (entity.CompareTag("Player"))
        {
            Task test = InteractWithPlayer();
            Task.Run(() => test, _cts.Token);
        }
        else
        {
            print("Interacting with NonPlayer");
        }
    }

Perhaps register your own cancellation delegate on the cancellation token??

Seems like that is just moving the lump around under the carpet, but there’s still a lump.

1 Like

Going to try that out, also going to create 2 scenes to compare Coroutines with access to the iterator, and async/await, to see which is cleaner,

thank you for your time Kurt!

1 Like

Use UniTask instead, it’s designed specifically to work with Unity and takes into account Unity’s threading context/loop. Also it’s alloc free, and have very nice extension methods for Unity (like awaiting Couroutines, DOTweens…).

2 Likes

@Terraya
You could also pass a cancellation condition directly to your task, without using the exception system. Then the task would have to check if that condition is true before stopping the work in progress and maybe doing some cleaning.

using System;
using System.Threading.Tasks;
using UnityEngine;

public class CancellingTask : MonoBehaviour
{
    public bool cancelTask = false;

    // Right click in the Inspector to start SomeHeavyTask
    [ContextMenu("StartTask")]
    void StartTask()
    {
        _ = SomeHeavyTask( () => cancelTask );
    }

    async Task SomeHeavyTask(  Func<bool> isCancelledFn = null   )
    {
        float timeToCompletion = 10.0f;

        Debug.Log("Doing SomeHeavyTask...");

        while ( timeToCompletion > 0.0f)
        {
            if( isCancelledFn != null && isCancelledFn())
            {
                Debug.Log("Task cancelled");
                return;
            }
            Debug.Log($"Completion in {timeToCompletion} seconds...");
            timeToCompletion -= 1.0f;
            await Task.Delay(1000);
        }

        Debug.Log("SomeHeavyTask DONE");
    }

}
2 Likes

Gonna take a look at UniTask, looks quite interessting

Yes, i did a Test Scene now where i want to see if i go for Coroutines or Tasks, now i have the same results either way, im going to post a bit later the Scripts so if people come across this topic, they can go for either burden,

for the Coroutine i have created a Wrapper Class since i need to access the iterator to know if the Coroutine is done or not, which is pretty simple and nice, i extract everything in the wrapper class, that way its much more cleaner.

For the Task i have done a Helper class to catch the exceptions, works like a charm, the only thing which i do not like is, if i have to work with cancellationTokens i will have to cache the Task beforehand …, since thats the case, i also have here a Wrapper Class which also works extremly well,

so i do not know if there is any need for UniTask but i will check it out since performance in my case is important