Unity - Cleaning up initialization boilerplate with the Sibling Component Attribute
The initialization of my unity components often look like this.
[RequireComponent(typeof(RigidBody2D))]
[RequireComponent(typeof(PlayerComponent))]
public sealed class MyGreatComponent : MonoBehaviour
{
private RigidBody2D _rigidBody;
// This component needs any collider, so RequireComponent doesn't work well when it could be children.
private Collider2D _collider;
private PlayerComponent _playerComponent;
// We don't require a weapon component - so its valid to be null.
private WeaponComponent _weaponComponent;
private void Awake()
{
_rigidBody = GetComponent<RigidBody2D>();
_collider = GetComponent<Collider2D>();
if (_collider == null)
{
Debug.LogError("No collision component found!");
}
_playerComponent = GetComponent<PlayerComponent>();
// No need to check for null since it's optional.
_weaponComponent = GetComponent<WeaponComponent>();
}
// Amazing stuff happening below...
}
And I got tired of the boilerplate in Awake
- having to type GetComponent
multiple of times and also potentially checking for null
each time.
So I made [SiblingComponent]
.
[RequireComponent(typeof(WeaponComponent))]
[RequireComponent(typeof(RigidBody2D))]
[RequireComponent(typeof(PlayerComponent))]
public sealed class MyGreatComponent : MonoBehaviour
{
[SiblingComponent]
private RigidBody2D _rigidBody;
[SiblingComponent]
private Collider2D _collider;
[SiblingComponent]
private PlayerComponent _playerComponent;
// No error message if we can't find the component.
[SiblingComponent(Optional = true)]
private WeaponComponent _weaponComponent;
private void Awake()
{
this.AssignSiblingComponents();
}
// Amazing stuff happening below...
}
[SiblingComponent]
automatically looks up a component of the specified type on the same GameObject and assigns it to the member variable. If it doesn’t exist, it’ll print an error. Optional = true
suppresses the error if it doesn’t exist.
This streamlined the initialization boilerplate for most of my components and made it much easier to add/remove dependencies on other sibling components.
The Implementation
Create a new file (I called it SiblingComponentAttribute.cs
) and let’s define our attribute class
[AttributeUsage(AttributeTargets.Field)]
[MeansImplicitUse]
public sealed class SiblingComponentAttribute : Attribute
{
public bool Optional = false;
}
Next, we’ll create AssignSiblingComponents
- the function that iterates through the [SiblingComponent]
attributes and looks up the correct component.
I chose to make this an extension method to have the syntax this.AssignSiblingComponents()
since I found it the easiest but you can also go the route of a normal function.
public static class SiblingComponentExtensions
{
// this is a Component since I didn't need it to be a MonoBehaviour. You can change this to a MonoBehaviour if you like.
public static void AssignSiblingComponents(this Component component)
{
// ...
}
}
First we need to get all fields with our [SiblingComponent]
attribute. I like using LINQ for this and making an iterator for it.
public static void AssignSiblingComponents(this Component component)
{
IEnumerable<FieldInfo> fields = component.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
.Where(prop => Attribute.IsDefined(prop, typeof(SiblingComponentAttribute)));
// ...
}
Next, let’s iterate through each property and try to find the component.
public static void AssignSiblingComponents(this Component component)
{
// ...
foreach (FieldInfo field in fields)
{
Type fieldType = field.FieldType;
// I don't support arrays of components with the code below but you can easily implement it with a bit more work.
if (fieldType.IsArray)
{
continue;
}
Type componentType = fieldType.IsArray ? fieldType.GetElementType() : fieldType;
Component[] siblingComponents = component.GetComponents(componentType);
// ...
}
}
Once we’ve tried to look up our component, if we’ve found it, assign it. Otherwise print an error if we need to.
public static void AssignSiblingComponents(this Component component)
{
// ...
foreach (FieldInfo field in fields)
{
// ...
Component[] siblingComponents = component.GetComponents(componentType);
if (siblingComponents.Length > 0)
{
field.SetValue(component, siblingComponents[0]);
}
else if (!field.GetAttribute<SiblingComponentAttribute>().Optional) // Only need to print an error if we're not optional.
{
// Feel free to customize this error message.
Debug.LogError($"{component.gameObject}: {component} - Unable to find sibling component of type {field.FieldType}");
}
}
}
And you’re done! Here’s the full AssignSiblingComponents
implementation:
public static void AssignSiblingComponents(this Component component)
{
IEnumerable<FieldInfo> fields = component.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
.Where(prop => Attribute.IsDefined(prop, typeof(SiblingComponentAttribute)));
foreach (FieldInfo field in fields)
{
Type fieldType = field.FieldType;
Type componentType = fieldType.IsArray ? fieldType.GetElementType() : fieldType;
Component[] siblingComponents = component.GetComponents(componentType);
if (siblingComponents.Length > 0)
{
field.SetValue(component, siblingComponents[0]);
}
else if (!field.GetAttribute<SiblingComponentAttribute>().Optional)
{
Debug.LogError($"{component.gameObject} {component} - Unable to find sibling component of type {field.FieldType}");
}
}
}
Here’s a GitHub Gist of the full implemenetation of SiblingComponentAttribute.as