Search code examples
c#genericsgame-developmentgodotgodot4

Godot C# - Use Generic Type as Signal Parameter


Throughout the course of my current project, I have found myself re-writing similar boilerplate in a lot of different classes for emitting a signal whenever a variable is changed. As an example for an integer variable, this code looks like:

[Signal] public delegate void ValueChangedEventHandler(int newValue);

private int _value;
public int Value
{
    get => _value;
    set
    {
        _value = value;
        EmitSignal(SignalName.ValueChanged, Value);
    }
}

By the 20th variable that I created using this format I started to tire of the clutter and repetition, so I figured I could create a helper class that wraps this functionality. I accomplished this with:

using Godot;

public partial class NotifyOnChange<[MustBeVariant]T> : RefCounted
{
    [Signal] public delegate void ChangedEventHandler(Variant newValue);
    
    private T _value;
    public T Value
    {
        get => _value;
        set
        {
            _value = value;
            EmitSignal(SignalName.Changed, Variant.From(Value));
        }
    }

    public NotifyOnChange() { }
    public NotifyOnChange(T value)
    {
        Value = value;
    }
}

This works quite well and does almost exactly what I was hoping for, but there is one annoying issue that I have and am hoping there is a workaround. The most important part of this system is the signal, as that is what numerous other scripts will be interacting with. However, the only way to declare this signal without getting errors is to make it of type Variant. This means that in each script that subscribes to this signal, the method signature that is expected for methods that are subscribed to this signal will look like:

private void OnValueChanged(Variant newValue)
{ 
    // Logic here...
}

My issue is that each of these methods will have the parameter as a Variant type, requiring casting to the desired type every time. This adds overhead as I will need to remember what type the value should be, and I have found that this adds more hassle than the benefit I get out of using this system.

So my question is:

Is there a way to declare a signal using a generic type, such as:

[Signal] public delegate void ChangedEventHandler(T newValue);

Trying to declare a signal in this way within the class I showed above results in Godot not generating the signal back-end and overall doesn't seem to work. I find this behaviour strange as I have marked the generic parameter T with the attribute [MustBeVariant] in the class declaration, so I would expect that there shouldn't be any problems using T in place of Variant for the signal parameter type.

Is there something I am missing here, or is this simply something that is not possible in Godot as of yet?


Solution

  • For anybody who comes across this, I still have not found a way to do this using Godot signals, but I was able to get this functionality to work using C# events.

    This was accomplished by simply declaring a delegate with a parameter of type T, and then creating an event using this delegate.

    To maximize compatibility and efficiency, I created a system that allows for the use of both the generic-typed event and the variant-typed signal. Overall, this looks like:

    using Godot;
    
    public partial class NotifyOnChange : RefCounted
    {
        [Signal] public delegate void VariantChangedEventHandler(Variant newValue);
        
        private Variant _variantValue;
        public Variant VariantValue
        {
            get => _variantValue;
            set
            {
                _variantValue = value;
                EmitSignal(SignalName.VariantChanged, VariantValue);
            }
        }
        
        public NotifyOnChange() { }
        public NotifyOnChange(Variant value)
        {
            VariantValue = value;
        }
    }
    
    public partial class NotifyOnChange<[MustBeVariant]T> : NotifyOnChange
    {
        public delegate void ChangedEventHandler(T newValue);
        public event ChangedEventHandler Changed;
        
        public T Value
        {
            get => VariantValue.As<T>();
            set => VariantValue = Variant.From(value);
        }
    
        public NotifyOnChange()
        {
            VariantChanged += newValue => Changed?.Invoke(newValue.As<T>());
        }
        
        public NotifyOnChange(T value)
        {
            Value = value;
        }
    }
    

    While it would be lovely to have official support from Godot for generic-typed signals, this workaround has functioned great and has provided a lot of flexibility!