@onready replacementI’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);
}
}