(C#) Awaiting on a method called through Godot.Object.Call

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By jjmontes

TL;DR

  • I’m trying to await a method I call via “Object.Call(name…)” in C#.
  • But I cannot get an Awaitable
  • How would I know if an async method I called is finished?

Background

I’m writing an events sequencer and I’d need to call methods, and ideally also set properties, much like AnimationPlayer does.

I need ot wait for the method to finish. The method could be async, so I was trying something like:

	object result = target.Call("RunStepText", "TestParameter");
	await Task.FromResult(result);

And also:

    await target.Call("RunStepText", "TestParameter");

And

	object result = target.Call("RunStepText", "TestParameter");
	await ToSignal(result, "completed");

But in C#, Object.Call returns a Variant and I do not really know how to deal with it.

I have read about the completed signal for method calls. Is this emitted for every function call? In any case, I haven’t been able to receive it either.

How can I call a method on an arbitrary object and check if the call is finished or if there are coroutines holding on to the call?

I’ll resort to use dedicated signals if needed but that would force me to write dedicated code for every object I want to automate via my sequencer.

:bust_in_silhouette: Reply From: voidshine

I’m trying to figure this out today as well. Engine call awaiter support in the vein of ToSignal could be neat, e.g. await ToComplete(...);

But do you need to use Call? You might be able to use C# async/await features more directly. I found two ways to do something similar to GDScript’s yield until coroutine “completed”. At least, these techniques work with my own C# functions…

  1. Use Task.StartNew or Task.Run to wrap a call and await that task. Note, this technique will execute the action on a separate thread which may cause some strange interactions with the engine if you call into it. This might be a good option if you have some heavy lifting to do outside of the game world, for example if you need to call web services or compute the ten billionth prime number.

  2. Use a regular C# async method and await a direct C# call to it. This will execute on the same thread and is usually what I want when breaking down game logic into composable asynchronous functions (coroutines). Basically, you let C# build the state machine and continuation flow for you…

async Task<bool> ConfirmPlayerLikesChocolate() {
    // Tween in the question label...
    await TweenLabel("Do you like chocolate?");
    // Pop the "Yes!" button.
    await FadeInButton(0.1f, "Yes!");
    // Wait 5 seconds to fade in the "No." button...
    await FadeInButton(5.0f, "No.");
    // Wait for player to select...
    // Ignore buttons because nobody dislikes chocolate.
    return true;
}

async Task AnnoyPlayer() {
    bool likesChocolate = await ConfirmPlayerLikesChocolate();
    if (!likesChocolate) {
        // CRASH!
    } else {
        // Oh yeah? What's your favorite kind of chocolate?!
    }
}

Note this brings the usual benefits of C#: composability, name checking, type safety, etc. Assigning the await expression to int instead of bool produces compile-time error.

You can write async void functions but can’t await them. Just change the return type to Task. Or, if you need to produce a result, Task<TResult> and return something of that type as usual.

Thank you. Hoever I needed to use Call, as my call sequences come from a data file, not code. The root cause of this is that Awaitables are not serializable into Variants, this will be fixed in an upcoming version according to the devs.

jjmontes | 2021-04-04 17:17

:bust_in_silhouette: Reply From: jarommadsen

I was able to solve this by casting result to Godot.Object.

GDScript MyGDScript = (GDScript) GD.Load("res://path_to_gd_file.gd");
Object myGDScriptNode = (Godot.Object) MyGDScript.New();
var result = (Godot.Object) myGDScriptNode.Call("async_method");
await ToSignal(result, "completed");
:bust_in_silhouette: Reply From: LauraSan

My call sometimes returned an object and sometimes a GDScriptFunctionState.
This worked for me (v3.5):

object obj = startConfiguration.Call("_callGdScript", param);

if(obj.GetType() == typeof(GDScriptFunctionState))
{
    object[] result = await ToSignal((GDScriptFunctionState)obj, "completed");
    GD.Print(result);
}
else
{
    GD.Print(obj);
}

Both prints resulted the string I needed.