Enhancing Godot Development with Metalama
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 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!