C# @onready replacement

I’ve adapted some work i found here: https://ruoyusun.com/2018/03/24/c-sharp-godot-reflection-copy.html to try and add the missing “on ready” functionality to C# when working with godot.

GDScript has the ability to add the @onready keyword which will run something in the _ready callback, usually assign a node to a field, which you have to do a lot.

I’ve adapted the code above to handle a number of cases:

This is a tedious operation which you often have to do in Godot so this removes quite a bit of boilerplate.

The code is below:

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Godot;

#nullable enable

public enum NodeAttributeType {
  AddChildNode,
  Parent,
  ChildNode,
  Path,
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class AddChildNodeAttribute : NodeAttribute {
  public AddChildNodeAttribute() {
    Action = NodeAttributeType.AddChildNode;
  }
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class ChildNodeAttribute : NodeAttribute {
  public ChildNodeAttribute() {
    Action = NodeAttributeType.ChildNode;
  }
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class ParentNodeAttribute : NodeAttribute {
  public ParentNodeAttribute() {
    Action = NodeAttributeType.Parent;
  }
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class GetNodeAttribute : NodeAttribute {
  public GetNodeAttribute(string? path = null) {
    Action = NodeAttributeType.Path;
    Path = path;
  }
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class NodeAttribute : Attribute {
  public string? Path;

  public NodeAttributeType Action = NodeAttributeType.Path;

  public NodeAttribute() {
  }

  public static void Ready(Node node) {
    var fields = node
        .GetType()
        .GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

    var properties = node
      .GetType()
      .GetProperties(
        BindingFlags.DeclaredOnly |
        BindingFlags.Public |
        BindingFlags.NonPublic |
        BindingFlags.Instance
      );

    var info = fields.Cast<MemberInfo>().Concat(properties);

    foreach (var field in info) {
      var getValue = () => field switch {
        PropertyInfo p => p.GetValue(node),
        FieldInfo f => f.GetValue(node),
        _ => throw new Exception("Unexpected field type")
      };

      var setValue = (object value) => {
        if (field is PropertyInfo p) {
          p.SetValue(node, value);
        }
        else if (field is FieldInfo f) {
          f.SetValue(node, value);
        }
        else {
          throw new Exception("Unexpected field type");
        }
      };

      var fieldType = Type () => field switch {
        PropertyInfo p => p.PropertyType,
        FieldInfo f => f.FieldType,
        _ => throw new Exception("Unexpected field type")
      };

      if (Attribute.GetCustomAttribute(field, typeof(NodeAttribute)) is NodeAttribute attr) {
        var action = attr.Action;

        switch (action) {
          case NodeAttributeType.ChildNode: {
              if (getValue() is Node childNode && !childNode.IsInsideTree()) {
                addChild(node, childNode);
              }
              else {
                // Find the first matching child
                Node? value =
                  (from child in node.GetChildren()
                   where child.GetType() == fieldType()
                   select child
                  ).Take(1).SingleOrDefault();

                if (value is not null) {
                  setValue(value);
                }
              }
              break;
            }

          case NodeAttributeType.Parent: {
              var value = node.GetParent();

              if (value.GetType() == fieldType()) {
                setValue(value);
              }
              else {
                Debug.Fail($"Couldn't set parent, {field.Name}({field.GetType()}) is not a {fieldType()}");
              }

              break;
            }

          case NodeAttributeType.Path: {
              var value = node.GetNodeOrNull(attr.Path ?? field.Name);

              if (value != null) {
                setValue(value);
              }

              break;
            }

          case NodeAttributeType.AddChildNode: {
              var childNode = getValue();
              addChild(node, childNode);
              break;
            }

          default: {
              break;
            }
        }
      }
    }
  }

  private static void addChild(Node node, object childNode) {
    if (childNode is Node child) {
      if (child.IsInsideTree()) {
        GD.PrintErr("Child is already inside tree");
        return;
      }
      else {
        node.AddChild(child);
      }
    }
    else {
      GD.PrintErr("Child is not a node");
    }
  }
}

We can make it easier to register all the nodes by adding some extension methods to nodes:

using Godot;
using System.Linq;

namespace ExtensionMethods;

public static class NodeExtensions {
  public static void WireNodes(this Node node) {
    NodeAttribute.Ready(node);
  }
}