landelare / ue5coro

A C++20 coroutine implementation for Unreal Engine 5 that feels almost native.
BSD 3-Clause Clear License
543 stars 48 forks source link

Canceling Nested Latent Awaiters #24

Closed DavidCampbellIII closed 6 months ago

DavidCampbellIII commented 6 months ago

Hello! Love this plugin, trying to get it to do all the equivalent things Unity's IEnumerator could handle.

I've got just about everything figured out except for canceling a latent coroutine that itself awaits other coroutines.

void UCoroTesterComponent::BeginPlay()
{
    Super::BeginPlay();

    co_parent = MakeShared<TCoroutine<>>(WaitParent({}));
    CancelParent({});
}

TCoroutine<> UCoroTesterComponent::WaitParent(FForceLatentCoroutine)
{
    while(this)
    {
        co_await Latent::Seconds(1.f);
    co_await WaitChild({});
    }
}

TCoroutine<> UCoroTesterComponent::WaitChild(FForceLatentCoroutine)
{
    UE_LOG(LogTemp, Warning, TEXT("Waiting Child"));
    co_await Latent::Seconds(1.f);
    UE_LOG(LogTemp, Warning, TEXT("NESTED"));
    co_await WaitNested({});
}

TCoroutine<> UCoroTesterComponent::WaitNested(FForceLatentCoroutine)
{
    UE_LOG(LogTemp, Warning, TEXT("Waiting NESTED"));
    co_await Latent::Seconds(1.f);
    UE_LOG(LogTemp, Warning, TEXT("Done NESTED"));
}

TCoroutine<> UCoroTesterComponent::CancelParent(FForceLatentCoroutine)
{
    co_await Latent::Seconds(5.f);
    UE_LOG(LogTemp, Warning, TEXT("Canceling Parent!"));
    co_parent->Cancel();
}

This was a test to determine the behavior of nested coroutines after the parent coroutine is canceled. It appears that even after WaitParent is canceled, WaitChild will continue to run, calling WaitNested.

Example output:

Waiting Child
NESTED
Waiting NESTED
Done NESTED
Waiting Child
Canceling Parent!  (at this point, I'd hope that the child would be auto-canceled as well!)
NESTED  (but we see it continues after it's await, and started to await on WaitNested)
Waiting NESTED
Done NESTED

Is this expected behavior, or am I doing something wrong? The only alternative I can think of is to just cancel all the coroutines on the object using:

GetWorld()->GetLatentActionManager().RemoveActionsForObject(this);

however I'd like the ability to only cancel 1 "set" instead of everything on the object if possible.

Thank you in advance! 😊

landelare commented 6 months ago

This is the intended behavior. If you want cancellation to spread or cancel a set of coroutines, you'll need to implement that yourself.

Interacting with the latent action manager to remove the latent actions is supported: latent coroutines do their best to behave exactly like BP latent actions would, given the limitations imposed by C++.

all the equivalent things Unity's IEnumerator could handle.

As a side note, this is an explicit anti-goal of this library. Other Unreal coroutine libraries exist whose design is closer to the Unity iterator hack.

DavidCampbellIII commented 6 months ago

Got it, thanks for the response!

Excuse my ignorance, but doesn't this project accomplish fundamentally the same thing that Unitys IEnumerators do?

This project certainly features a more rich feature set compared to that of Unity, which is awesome, but everything I'm used to doing in Unity was easily achieved with this project, excluding the cascading cancelations of nested coroutines.

If you're able to point out any other projects that are more closely emulating Unitys way of doing things, I'd greatly appreciate it! All my own searches have lead me back to this project, as nothing else even came close.

Thanks again!

landelare commented 6 months ago

No, it mainly wants to bring async/await into Unreal. There's a fundamental inversion of ownership and control between the two: an iterator(C#)/generator(C++) is controlled by its caller and relies on its "polling" (MoveNext() in .NET land) to continue, while an async method is expected to run independently.