Editor Plugin to Connect Signals Through Viewport

Godot Version

4.7.2

Question

Alongside our game project I’m authoring some editor tools to help the development process. I’m building an interaction system through signals. Having used many level editors for game modding over the years, I wanted to recreate a cool feature that could prove useful for our development. So the goal is to select one object in the 3D viewport, Ctrl-Alt-Click another object and thus connect a specific signal this way.

I pick the object to connect via ray cast from the mouse position into the 3D viewport. Disconnecting already existing signals is rather simple:

Node targetInteractable = hit.Owner;
// if already connected, disconnect
var connection = selectedNode.GetSignalConnectionList("InteractionFinished")
    .FirstOrDefault(d =>
    {
        var callable = d["callable"].AsCallable();
        return callable.Target == targetInteractable;
    });
    if (connection != null)
    {
        var signal = connection["signal"].AsSignal();
        var callable = connection["callable"].AsCallable();
        selectedNode.Disconnect(signal.Name, callable);
        EditorInterface.Singleton.GetSelection().RemoveNode(selectedNode); //update inspector
        EditorInterface.Singleton.GetSelection().AddNode(selectedNode);
        return (int)AfterGuiInput.Stop;
    }

This works and signals are properly updated in the inspector.
Now I’m not quite sure how to connect them. The signal/event names are always the same, but trying to call

selectedNode.Connect("InteractionFinished", new Callable(targetInteractable, "InteractionReceivedEvent"));

does… “something”. It connects the node somehow, as shown through my visual debugging, but the inspector doesn’t show that the signal I want to connect is actually connected. It’s not a refresh issue either because the action in-game doesn’t work.

One thing I notices is that I can’t really cast nodes to their script class via editor plugin. Getting the script itself via GetScript() on the node returns the correct C# class, just casting won’t work. Maybe that’s also the reason why signal connection doesn’t quite work the way I hope it would?

Anyone have any ideas on this?

Try passing the ConnectFlags.CONNECT_PERSIST in the connect() function.

Yes, thanks! That creates the connection I want!

Now additionally, is there a way to try-cast the selected node to the attached C# script, so I can rely on C#'s strong typing to infer if the node is actually connectable?

As alternative, I checked the engine code for the Signal dock and found that there the actual script source is parsed and checked for functions. While I could do that if all else fails, I’d for sure prefer to stay within my own realm of well defined and available classes within my project.

Also I was wondering about how arguments are handled. If the signal’s args and the callable’s method match, should it “just work”? Because in my base interaction class I define the following method:

public virtual void InteractionReceivedEvent(CharacterBase character)
{
    ...
}

And the signal to emit passes an argument of the same type:

[Signal] 
public delegate void InteractionFinishedEventHandler(CharacterBase character);

However, connecting the signal to the method gives me the following error:

E 0:00:04:696   NativeCalls.cs:8163 @ int Godot.NativeCalls.godot_icall_2_921(nint, nint, Godot.NativeInterop.godot_string_name, System.ReadOnlySpan`1[Godot.Variant], Godot.NativeInterop.godot_string_name): Error calling from signal 'InteractionFinished' to callable: 'Node3D(AnimatedInteraction.cs)::InteractionReceivedEvent': Method not found.
  <C++ Source>  core/object/object.cpp:1311 @ emit_signalp()
  <Stack Trace> NativeCalls.cs:8163 @ int Godot.NativeCalls.godot_icall_2_921(nint, nint, Godot.NativeInterop.godot_string_name, System.ReadOnlySpan`1[Godot.Variant], Godot.NativeInterop.godot_string_name)
                GodotObject.cs:719 @ Godot.Error Godot.GodotObject.EmitSignal(Godot.StringName, Godot.Variant[])
                AnimatedInteraction.cs:28 @ void ModuloGame.System.Interaction.AnimatedInteraction.<_Ready>b__25_0()
                Callable.generics.cs:39 @ void Godot.Callable.<From>g__Trampoline|1_0(object, Godot.NativeInterop.NativeVariantPtrArgs, Godot.NativeInterop.godot_variant&)
                DelegateUtils.cs:86 @ void Godot.DelegateUtils.InvokeWithVariantArgs(nint, System.Void*, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_variant*)

The node signal is connected like this, currently without prior checks whether methods exist, but I tested it on nodes that have the required scripts and its methods:

selectedNode.Connect("InteractionFinished",
                                new Callable(targetInteractable, "InteractionReceivedEvent"), (uint)ConnectFlags.Persist);

Removing the argument from the “InteractionReceivedEvent” makes it work, but I don’t want to miss out on passing arguments just because I don’t know how it works.

It’s been two weeks, I hope it’s okay to bump this thread.