Skip to content

Instantly share code, notes, and snippets.

@nicolasmaclean
Last active August 4, 2023 00:39
Show Gist options
  • Save nicolasmaclean/03692f0ea16322c97457ea977d98ef89 to your computer and use it in GitHub Desktop.
Save nicolasmaclean/03692f0ea16322c97457ea977d98ef89 to your computer and use it in GitHub Desktop.
System for dependency injection in Unity. Everything revolves arround the [Dependency] attribute. The SystemBehaviour class can be derived from or the CollectDependencies method can be called directly to perform injection.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
[AttributeUsage(validOn: AttributeTargets.Field)]
public class DependencyAttribute : Attribute
{
public enum ELocation
{
Self, Child, Parent,
}
public ELocation Location { get; }
public bool Optional { get; }
/// <summary>
/// Intended for use with <see cref="SystemBehaviour.CollectDependencies"/>
/// Marks field with information use during dependency injection.
/// </summary>
/// <param name="location"></param>
/// <param name="optional"></param>
public DependencyAttribute(ELocation location = ELocation.Self, bool optional = false)
{
Location = location;
Optional = optional;
}
}
/// <summary>
/// Dependency injection for hierarchical <see cref="Component"/>s. Fields with the <see cref="DependencyAttribute"/>
/// are injected. <see cref="CollectDependencies"/> can be manually called to control the timing of injection or
/// <see cref="SystemBehaviour"/> may be inherited from to automatically do so during <see cref="Awake"/>
/// </summary>
public abstract class SystemBehaviour : MonoBehaviour
{
/// <summary>
/// Injects fields on <paramref name="component"/> marked with <see cref="DependencyAttribute"/>.
/// </summary>
/// <param name="component"> Object to be inspected and injected. </param>
public static void CollectDependencies(Component component)
{
foreach (var field in GetDependencies(component))
{
// validate field
var fieldType = field.FieldType;
bool fieldTypeIsInvalid = !fieldType.IsSubclassOf(typeof(Component));
if (fieldTypeIsInvalid)
{
Debug.LogError($"{component.GetType()} has {nameof(DependencyAttribute)} applied to " +
$"{field.Name}. {field.Name} is {fieldType} but only types deriving from " +
$"{nameof(Component)} are supported.");
continue;
}
// get dependency
DependencyAttribute attr = (DependencyAttribute) Attribute.GetCustomAttribute(field, typeof(DependencyAttribute));
Component dependency = GetDependency(component, attr.Location, fieldType);
// validate dependency
if (dependency == null)
{
if (!attr.Optional)
{
var message = $"{component.name} is missing a {fieldType} component";
Debug.LogError(attr.Location switch
{
DependencyAttribute.ELocation.Self => $"{message} on itself.",
DependencyAttribute.ELocation.Child => $"{message} among its children",
DependencyAttribute.ELocation.Parent => $"{message} among its ancestors",
_ => throw new ArgumentOutOfRangeException(),
});
}
continue;
}
// configure field
field.SetValue(component, dependency);
}
}
static IEnumerable<FieldInfo> GetDependencies(Component component)
{
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
return from field in component.GetType().GetFields(bindingFlags)
where Attribute.IsDefined(field, typeof(DependencyAttribute))
select field;
}
static Component GetDependency(Component component, DependencyAttribute.ELocation location, Type type)
{
Component t = location switch
{
DependencyAttribute.ELocation.Self => component.GetComponent(type),
DependencyAttribute.ELocation.Child => component.GetComponentInChildren(type),
DependencyAttribute.ELocation.Parent => component.GetComponentInParent(type),
_ => throw new ArgumentOutOfRangeException(),
};
// GetComponent returns an object that override the null operator to show its invalid
// for our purposes it should be literal null
return t ? t : null;
}
/// <summary>
/// Perform dependency injection.
/// </summary>
[ContextMenu("Collect System Dependencies")]
protected virtual void Awake() => CollectDependencies(this);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment