Last active
August 4, 2023 00:39
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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