Skip to content

Improve usability of signals in C# using code generation #6705

Description

@pavlexander

Describe the project you are working on

irrelevant/any game

Describe the problem or limitation you are having in your project

When working with signals - there are a couple of things could be improved on C# side using the code generation.

Signal names

when referencing a signal name of another class you have to prepend the class name for signal to be discovered.

example signal in class 1:

    [Signal]
    public delegate void StartCookingEventHandler(int foodName);

then in class 2 you have specify the class name explicitly:

class1.Connect(Class1.SignalName.StartCooking, new Callable());

in contrast this is how you'd connect to a signal inside the same class 1

this.Connect(SignalName.StartCooking, new Callable());

or of course, the worst of them all is to just use the string:

this.Connect("StartCooking", new Callable());

You might not see an immediate problem with any of this and there is no issue really. Subjectively I think that there is a lot of unnecessary code that could be omitted and also, specifying the signal name as the string should be forbidden :)

Arguments

The second issue, and the main issue that I would like to be solved or at least addressed is argument safety. Currently it's non-existent. You will only know of the problem after building and running the code. Consider the same signal:

    [Signal]
    public delegate void StartCookingEventHandler(int foodName);

then you need to invoke it like this (any of 3 options):

    public override void _Ready()
    {
        this.Connect(SignalName.StartCooking, Callable.From((int foodName) => Cook(foodName)));
        this.Connect(SignalName.StartCooking, new Callable(this, nameof(Cook)));
        this.Connect(SignalName.StartCooking, Callable.From<int>(Cook));
    }

    public void Cook(int foodName)
    {

    }

but then if let's say the signature of the signal itself is changed, then your code will compile just fine:

public partial class Class1 : Entity
{
    [Signal]
    public delegate void StartCookingEventHandler(int foodName, int recepyName, string authorName);

    public override void _Ready()
    {
        this.Connect(SignalName.StartCooking, Callable.From((int foodName) => Cook(foodName)));
        this.Connect(SignalName.StartCooking, new Callable(this, nameof(Cook)));
        this.Connect(SignalName.StartCooking, Callable.From<int>(Cook));
    }

    private void Cook(int foodName)
    {
    }
}

same problem is with Emit method arguments - it accepts just about anything!

class1.EmitSignal(Class1.SignalName.StartCooking, 1);

arguments safety is non-existent.

Conclusion

when it comes to signals there is a lot of boilerplate, non-manageable, non-safe code and I have a proposal on how to solve it.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

My proposal basically suggests to extend the code generation logic, to generate additional methods and to help the end-user make less mistakes.

  • firstly the user will not be required to fill in the signal name
  • secondly the user will will have an argument/signature safety

The biggest benefit is if the signal signature is changed - the user will know of the issue not at the run-time but at a compile time.

p.s. I have never touched the GDScript version of Godot (Sorry ;( ) so I cannot speak for that half of the users, but in C# it's possible to make dev life a lot easier and I will explain how.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Consider a simple signal example:

    [Signal]
    public delegate void StartCookingEventHandler(int foodName, int recepyName, string authorName);

the methods that need to be generated are:

  • one for connecting to a method, and
  • second is for emitting
    public void ConnectStartCooking(Action<int, int, string> method)
    {
        this.Connect(SignalName.StartCooking, Callable.From<int, int, string>(method));
    }

    public void EmitStartCooking(int foodName, int recepyName, string authorName)
    {
        this.EmitSignal(SignalName.StartCooking, foodName, recepyName, authorName);
    }

we could use the same name convetion as with the EventHandler meaning that new method names will start with Connect or Emit and then continue with the whatever the original name of the signal is, with the EventHandler omitted.

Now when you need to connect to the signal of another class instead of this:

public partial class Class2 : Entity
{
    public override void _Ready()
    {
        var class1 = new Class1();

        class1.Connect(Class1.SignalName.StartCooking, new Callable()); // non-argument-safe, long
        class1.EmitSignal(Class1.SignalName.StartCooking, 1); // non-argument-safe, long
    }

    private void Cook(int foodName, int recepyName, string authorName)
    {
    }
}

you can do this:

        var class1 = new Class1();

        class1.ConnectStartCooking(Cook); // argument safe, short
        class1.EmitStartCooking(1, 2, "2"); // argument safe, short

If this enhancement will not be used often, can it be worked around with a few lines of script?

can be worked around by typing a lot of non-safe code

Is there a reason why this should be core and not an add-on in the asset library?

The biggest benefit of type-safe programming languages (hello Javascript) is the ability to see errors before they occur. By using signal names as strings and by using methods that allow us to enter any set of arguments for a very specific method's signature is atrocious and should be forbidden if possible. Luckily in C# we got code generators.

This deserves to be a core feature of the already awesome product to make it even better!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions