Cat-Lips / GodotSharp.SourceGenerators

C# source generators for the Godot Game Engine
MIT License
128 stars 13 forks source link

Automatic signals for fields #52

Closed InfiniteCactusDev closed 9 months ago

InfiniteCactusDev commented 9 months ago

Hey! I'm working on something, but I'm a bit stuck. Posting it here because a) I think it could be a great addition to this project and b) you seem to have a lot of experience in this field and are hopefully able to help.

So what I'm working on is automatic boiler plating generation to connect Signals to fields.

It currently reads this:

[Signalize] private float _range = 80.0f;

And generates this (in a partial class):

[Signal] public delegate void RangeChangedEventHandler(float range);
public float Range
{
    get => _range;
    set {
        _range = value;
        EmitSignal("RangeChanged", value);
    }
}

This works fine - but where I think it's going wrong is that Godot also uses source generators to create code based on the Signal attribute. And because my Signal attribute is added through source generation Godot's doesn't find it and the Signal won't work...

Do you have any idea's?

Edit: I found where Godot creates the Signal magic here: https://github.com/godotengine/godot/blob/5f05e2b9b1a3fedcdd7ecb2ab976a2687fd6f19a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs#L17 Perhaps I can add my own logic to a copy of this script..

Cat-Lips commented 9 months ago

Hi IC.

Yep, you're absolutely correct. Source generators can only operate on original code. They run independently from each other and in parallel, so don't have access to generated code. Partial methods can be a good workaround, but that won't help here. The problem with adding something like a property is that other Godot generators create code to get/set values, generate property names, etc, so if you did want to generate something used by a Godot generator, you would need to replicate the functionality of all generators (or at least more than just the Signal generator). There is a project setting that turns of Godot generators for this purpose, but I wouldn't recommend it as the way forward.

The good news is, however, since signals are effectively the gdscript equivalent of C# events, you'd only need to generate the signal functionality if you needed to connect to them via the editor, otherwise it might be sufficient just to handle it all in C#. If that is the case, there is a [Notify] attribute in this package will generate a change event if the value changes. It could be extended to include signal functionality if it is required. I had to put it on the property instead of the field so it could be used with [Export] to track editor changes.

Usage looks like this and generates public event Action MyValueChanging/MyValueChanged:

    [Tool, GlobalClass]
    public partial class MyData : Resource
    {
        [Export, Notify] public float MyValue { get => _myValue.Get(); set => _myValue.Set(value); }
    }

Or you could use something like the following extension method:

    internal static class SetExtensions
    {
        public static void Set<T, TValue>(this T src, ref TValue field, TValue value, params Action[] onSet)
            => src.Set(ref field, value, notify: false, onSet);

        public static void Set<T, TValue>(this T src, ref TValue field, TValue value, bool notify, params Action[] onSet)
        {
            if (!Equals(field, value))
            {
                field = value;

                OnSet();
                OnChanged();
                NotifyEditor();
            }

            void OnSet()
                => onSet?.ForEach(x => x?.Invoke());

            void OnChanged()
                => (src as Resource)?.EmitSignal(Resource.SignalName.Changed);

            void NotifyEditor()
            {
                if (notify)
                    (src as GodotObject)?.NotifyPropertyListChanged();
            }
        }

Usage:

    [Tool, GlobalClass]
    public partial class MyData : Resource
    {
        private float _myValue = .5f;
        public event Action MyValueSet;
        [Export] public float MyValue { get => _myValue; set => this.Set(ref _myValue, value, OnMyValueSet, MyValueSet); }

        private void OnMyValueSet()
        {
            // Do internal stuff, eg, connect to Changed event if Resource, etc
        }
    }

Note that the above extension method only triggers events when a value is set (ie, doesn't check for Resource or Resource[]), whereas the Notify attribute triggers events when the value is changed (and does check for Resource and Resource[]). Pros and cons...

InfiniteCactusDev commented 9 months ago

Thanks a bunch for your response! I had ventured a bit deeper into this in the meantime and made my own version of the ScriptSignalsGenerator (disabling the original like you mentioned) that properly connects the Signal to the editor. Only to then find out that now it doesn't work well with the Export attribute, which would require to modify ScriptPropertiesGenerator and possibly others as well (just like you mentioned)...

I had seen, and was inspired by, your Notify implementation, but naively thought it would be easy to make something that would reduce boiler plating and work seamlessly with the editor.

i will definitely take a look into your suggested code when I have the time!

Thanks again!