I am trying to create an interaction and dialogue system in Godot 4 using C#. Using the interaction system, I want to interact with an NPC which will trigger quest dialogue. The dialogue system seems to be working fine, but i am running into issues regarding the interaction system using async/await. Here is the code:
public async override void _Input(InputEvent @event)
{
if (@event.IsActionReleased("Interact") && canIneract && activeAreas.Count > 0)
{
canIneract = false;
interactDisplay.Hide();
var result = activeAreas[0].Interact.Call(otherBody);
await ToSignal((GodotObject)result, "DialogueComplete");
canIneract = true;
}
}
I am translating GDSript code from a tutorial I am watching, and their code looks like this:
func _input(event):
if event.is_action_pressed("interact") && can_interact:
if active_areas.size() > 0:
can_interact = false
label.hide()
await active_areas[0].ineract.call()
can_interact = true
There is slight variation from my code to his, but the await
line is what matters. I am trying to await
on my interaction system until my dialogue system finishes. However, in C#, I have to use the ToSignal method in order to await like you see in the first example. I am a bit confused because the way you implement the same actions in GDScript can be very deferent from the C# version. I have tried to look online for a solution and to read the documentation, and the only hint that I got is to use the ToSignal method, and pass in the signal name that I want. But I keep getting this error.
E 0:00:05:0142 Godot.NativeInterop.NativeFuncs.generated.cs:108 @ Godot.Error Godot.NativeInterop.NativeFuncs.godotsharp_internal_signal_awaiter_connect(IntPtr , Godot.NativeInterop.godot_string_name& , IntPtr , IntPtr ): Parameter "p_source" is null.
<C++ Source> modules/mono/signal_awaiter_utils.cpp:37 @ gd_mono_connect_signal_awaiter()
<Stack Trace> Godot.NativeInterop.NativeFuncs.generated.cs:108 @ Godot.Error Godot.NativeInterop.NativeFuncs.godotsharp_internal_signal_awaiter_connect(IntPtr , Godot.NativeInterop.godot_string_name& , IntPtr , IntPtr )
SignalAwaiter.cs:18 @ Godot.SignalAwaiter..ctor(Godot.GodotObject , Godot.StringName , Godot.GodotObject )
GodotObject.base.cs:174 @ Godot.SignalAwaiter Godot.GodotObject.ToSignal(Godot.GodotObject , Godot.StringName )
InteractionManager.cs:88 @ void InteractionManager+<_Input>d__15.MoveNext()
:0 @ void System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start<TStateMachine >(TStateMachine& )
:0 @ void InteractionManager._Input(Godot.InputEvent )
Node.cs:2075 @ Boolean Godot.Node.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
CanvasItem.cs:1374 @ Boolean Godot.CanvasItem.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
Node2D.cs:516 @ Boolean Godot.Node2D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
InteractionManager_ScriptMethods.generated.cs:66 @ Boolean InteractionManager.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
CSharpInstanceBridge.cs:24 @ Godot.NativeInterop.godot_bool Godot.Bridge.CSharpInstanceBridge.Call(IntPtr , Godot.NativeInterop.godot_string_name* , Godot.NativeInterop.godot_variant** , Int32 , Godot.NativeInterop.godot_variant_call_error* , Godot.NativeInterop.godot_variant* )
I dont know if my signal name is correct, and I have tried everything, but nothing seems to work. The Signal I am referenceing is from my dialogue manager called DialogueManagerEventHandler, but apparently DialogueManager is not the signal name.
At this point, I dont know if I am going in the right direction, if what I am looking for just simply is not implemented in the C# API, or maybe if I am making a simple spelling error. Any thoughts?
They seem to be using await
for a coroutine instead of a signal (notice that what they are passing to await
is not a signal).
The way it works behind the scenes is that when the GDScript reaches an await
it returns a GDScriptFunctionState
, which encodes the position on the script to resume, and is scheduled for continuation. On the caller side, await
will resume on the completed
signal of the GDScriptFunctionState
.
So, assuming this works from C#, try the Apparently it does not work.completed
signal.
Anyway, if you are implementing this fully in C#, I'd say work with Func<T, Task>
or similar instead of Callable
.
In fact, C# tasks works on similar ideas※... If you write an async
method in C#, the compiler will rewrite it as a series of continuations. When executing the code, the first block returns a Task
, and schedules the next continuation, when the final continuation finishes it sets the Task
completed.
Addendum based on comments
I am looking for is waiting for a particular signal to trigger so the code can then continue on
You already know how to await for a signal: you use ToSignal
.
Yet, this is not awaiting on a signal:
await active_areas[0].ineract.call()
In GDScript await
has two use cases: it can await on a signal, or a coroutine. And this seems to be the latter. See Awaiting for signals or coroutines.
On the other hand, your code in C# is awaiting a signal from the object returned by calling Interact
:
var result = activeAreas[0].Interact.Call(otherBody);
await ToSignal((GodotObject)result, "DialogueComplete");
Thus... The code that Interact
points to needs to return an object that emits that particular signal.
Notice that if you emit the signal before returning from whatever Interact
calls, the code that calls it (the one shown above) won't be awaiting the signal yet (because the code has not returned yet).
So, to be clear: the signal must be emitted by the object returned by Interact
AFTER Interact
returns it.
One work around would be use to a deferred call inside the code pointed by Interact
to emit the signal.
I should also mention that not only am I trying to wait until a particular task is completed, but that task calls other methods, some of which have to be activated with input.
You can have an object that handles input and emits a signal, and then you await on that signal.