Enhancing Godot Development with Metalama

by Philip Rotter on 29 Jan 2025

Godot is an amazing open-source game engine that provides developers with the tools to build games efficiently. While GDScript is Godot’s native language, many developers prefer to use C# for its rich ecosystem and the ability to leverage third-party libraries. One such library is Metalama, a powerful metaprogramming framework that can streamline repetitive or error-prone tasks.

In this article, we’ll explore how to use Metalama to simplify property usage customization in Godot. Specifically, we’ll address a common problem: defining property usage for exported properties in the Godot editor.

About the author

Philip Rotter Philip Rotter is a full-stack developer and passionate game creator with a deep knowledge of diverse game engines and development tools. Currently, he’s focused on building Gradual Warfare, a mobile game developed using the Godot Engine. Gradual Warfare is a future-punk strategy game that blends auto-battle and idle mechanics for an engaging and dynamic player experience. Connect with him on LinkedIn.

The Problem: Boilerplate Code for Property Usage

The Godot editor allows developers to customize components and resources by overriding the _ValidateProperty method and setting flags that define property behavior. However, managing property usage flags often results in repetitive and boilerplate-heavy code.

For example, imagine you want to create a class with an exported string property that’s only useful in the editor and another property that is hidden in the editor but serialized for runtime use. Here’s how you would traditionally handle this:

[GlobalClass]
[Tool]
public partial class ArmyContainer : Resource
{
    [Export]
    private string ExportString;

    [Export]
    public bool ContainsHello;

    public override void _ValidateProperty(Dictionary property)
    {
        if (property["name"].AsStringName() == nameof(ExportString))
        {
            property["usage"] = (int)PropertyUsageFlags.Editor;
        }
        else if (property["name"].AsStringName() == nameof(ContainsHello))
        {
            property["usage"] = (int)PropertyUsageFlags.NoEditor;
        }
    }
}

This approach works but quickly becomes cumbersome as the number of properties grows. What if we could eliminate this boilerplate and simplify the code?

The Solution: Attributes and Metalama

Using Metalama, we can replace the repetitive code with clean, declarative attributes. Here’s what the rewritten class will look like:

[GlobalClass]
[Tool]
public partial class ArmyContainer : Resource
{
    [Export]
    [OnlyInEditor]
    private string ExportString;

    [Export]
    [HideInEditor]
    public bool ContainsHello;
}

Much cleaner, right? With the help of Metalama, we’ll make these attributes automatically handle property usage without manually overriding _ValidateProperty.

Step 1: Define Custom Attributes

First, we’ll define the base attribute, GenPropertyUsageAttribute, which will serve as the foundation for specific attributes like OnlyInEditor and HideInEditor.

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
[RunTimeOrCompileTime]
public class GenPropertyUsageAttribute : Attribute
{
    // Default value: 6 (store and show in editor)
    public int PropertyUsageFlags { get; set; } = 6;

    public GenPropertyUsageAttribute() { }

    // Constructor to override default PropertyUsageFlags
    public GenPropertyUsageAttribute(int propertyUsageFlags)
    {
        PropertyUsageFlags = propertyUsageFlags;
    }
}

Now, let’s create two derived attributes for common use cases:

  • OnlyInEditor: The property is visible in the editor but not stored.
  • HideInEditor: The property is stored but not visible in the editor.
[RunTimeOrCompileTime]
public class OnlyInEditorAttribute : GenPropertyUsageAttribute
{
    public static new int PropertyUsageFlags { get; set; } = 4; // Editor-only, no storage

    public OnlyInEditorAttribute() : base(PropertyUsageFlags) { }
}

[RunTimeOrCompileTime]
public class HideInEditorAttribute : GenPropertyUsageAttribute
{
    public new static int PropertyUsageFlags { get; set; } = 2; // No editor, storage only

    public HideInEditorAttribute() : base(PropertyUsageFlags) { }
}

Step 2: Create the Aspect

Next, we’ll create a Metalama aspect that scans for attributes like OnlyInEditor and HideInEditor, then injects logic into the _ValidateProperty method to apply the appropriate PropertyUsageFlags.

internal sealed class GenPropertyUsageTypeAspect : IAspect<INamedType>
{
    public void BuildEligibility(IEligibilityBuilder<INamedType> builder)
    {
        builder.MustHaveAttributeOfType(typeof(ToolAttribute));
        builder.MustBeConvertibleTo(typeof(GodotObject));
    }

    public void BuildAspect(IAspectBuilder<INamedType> builder)
    {
        // Select fields that have the [GenPropertyUsage] attribute.
        var fields = builder.Target.AllFieldsAndProperties
            .Where(property =>
                property.Attributes.OfAttributeType(typeof(GenPropertyUsageAttribute)).Any());

        // Skip the aspect if we don't have any field.
        if (!fields.Any())
        {
            builder.SkipAspect();
            return;
        }

        // Introduce a method named _ValidateProperty 
        // using the template ValidatePropertyTemplate defined below.
        builder.IntroduceMethod(
            nameof(this._ValidateProperty),
            tags: new { Fields = fields },
            whenExists: OverrideStrategy.Override );
    }

    // Template for the new _ValidateProperty method.
    [Template]
    public void _ValidateProperty(Dictionary property)
    {
        // Call the base method or the hand-written implementation, if any.
        meta.Proceed();

        var name = property["name"].AsStringName();

        // Enumerate all fields identified in the BuildAspect method.
        // (This loop executes at build time.)
        foreach (var classFieldOrProperty in (IEnumerable<IFieldOrPropertyOrIndexer>)meta.Tags["Fields"])
        {
            if (name == classFieldOrProperty.Name)
            {
                var attribute = classFieldOrProperty
                    .Attributes
                    .GetConstructedAttributesOfType<GenPropertyUsageAttribute>()
                    .First();

                property["usage"] = attribute.PropertyUsageFlags;
            }
        }
    }
}

Step 3: Create the Fabric

Finally, we’ll use a Metalama fabric to apply our aspect to all eligible classes.

internal sealed class GenPropertyUsageTypeFabric : ProjectFabric
{
    public override void AmendProject(IProjectAmender amender)
    {
        amender
            .SelectDeclarationsWithAttribute(typeof(ToolAttribute))
            .OfType<INamedType>()
            .AddAspectIfEligible(t => new GenPropertyUsageTypeAspect());
    }
}

Testing the Result

To test the implementation, you can create a Tool script in Godot with annotated properties:

[GlobalClass]
[Tool]
public partial class ArmyContainer : Resource
{
    [Export]
    [OnlyInEditor]
    private string ExportString;

    [Export]
    [HideInEditor]
    public bool ContainsHello;
}

In the Godot editor:

  • ExportString will be visible but not serialized.
  • ContainsHello will be serialized but not visible in the editor.

This approach eliminates boilerplate code and makes property management much cleaner.


Conclusion

With Metalama, we extended Godot’s functionality by automating the customization of property usage. This approach saves time, reduces boilerplate, and minimizes errors. By leveraging attributes and metaprogramming, we can focus on creating games rather than managing tedious editor logic.

Stay tuned for more tutorials on how to enhance Godot development with powerful tools like Metalama. Have fun coding!

This article was first published on a https://blog.postsharp.net under the title Enhancing Godot Development with Metalama.