├── Scripts ├── UnityDependencyInjection.asmdef ├── InjectAttribute.cs.meta ├── DependencyContainer.cs.meta ├── IDependencyDestructionHandler.cs.meta ├── IgnoreSceneInjectionAttribute.cs.meta ├── IDependencyInjectionCompleteHandler.cs.meta ├── IDependencyDestructionHandler.cs ├── InjectAttribute.cs ├── IDependencyInjectionCompleteHandler.cs ├── IgnoreSceneInjectionAttribute.cs ├── UnityDependencyInjection.asmdef.meta └── DependencyContainer.cs ├── CHANGELOG.md.meta ├── README.md.meta ├── package.json.meta ├── LICENSE.md.meta ├── Scripts.meta ├── package.json ├── CHANGELOG.md ├── LICENSE.md └── README.md /Scripts/UnityDependencyInjection.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityDependencyInjection" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fea1cff0705b48c9b2b0a0f4c85eb9f8 3 | timeCreated: 1603807071 -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 12a7fff51bbe4e75be09da0486aac9d0 3 | timeCreated: 1567435813 -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d4b284bb8ea04244b233962681dffc5e 3 | timeCreated: 1567587842 -------------------------------------------------------------------------------- /Scripts/InjectAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f594119189e54e109bf3376adcfdad42 3 | timeCreated: 1539769132 -------------------------------------------------------------------------------- /Scripts/DependencyContainer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c905a9d742704bb6a320b607b327fa8e 3 | timeCreated: 1539766585 -------------------------------------------------------------------------------- /Scripts/IDependencyDestructionHandler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff582f4046c34a4da7824a013fb1b73c 3 | timeCreated: 1545137980 -------------------------------------------------------------------------------- /Scripts/IgnoreSceneInjectionAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9c26cbc10b2d42218461faae0cd04911 3 | timeCreated: 1585153616 -------------------------------------------------------------------------------- /Scripts/IDependencyInjectionCompleteHandler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2fa6e3848f08409cb185fbac4bfa6a87 3 | timeCreated: 1539782591 -------------------------------------------------------------------------------- /Scripts/IDependencyDestructionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace UnityDependencyInjection 2 | { 3 | public interface IDependencyDestructionHandler 4 | { 5 | void HandleDependenciesDestroyed(); 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c68daac59093438f9caa7f06ffeb290f 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts/InjectAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityDependencyInjection 4 | { 5 | [AttributeUsage(AttributeTargets.Field)] 6 | public class InjectAttribute : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /Scripts/IDependencyInjectionCompleteHandler.cs: -------------------------------------------------------------------------------- 1 | namespace UnityDependencyInjection 2 | { 3 | public interface IDependencyInjectionCompleteHandler 4 | { 5 | void HandleDependencyInjectionComplete(); 6 | } 7 | } -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d2d4ace9bbdaa3742abf1b1f89dfd325 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/IgnoreSceneInjectionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityDependencyInjection 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class IgnoreSceneInjectionAttribute : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /Scripts/UnityDependencyInjection.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4482c9cec569e1c45af677c1c55e1b7e 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.kkjamie.unity-dependency-injection", 3 | "displayName": "UnityDependencyInjection", 4 | "version": "1.3.2", 5 | "description": "", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/kkjamie/unity-dependency-injection" 9 | }, 10 | "author": "Jamie Healey" 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3.2] - 2021-12-03 4 | ### Changes 5 | - Added better error handling to the `IDependencyInjectionCompleteHandler`. 6 | - Added `EnableLogging` property, and added several logs. 7 | - Refactored the type cache, improving injection performance. 8 | - Identified optimization opportunity and documented it with comments. 9 | 10 | ## [1.3.1] - 2021-09-07 11 | ### Changes 12 | - Added LICENSE.md 13 | 14 | ## [1.3.0] - 2020-10-27 15 | ### Changes 16 | - `InjectToSceneObjects` can now optionally inject to inactive game objects (default behaviour is to include inactive objects) 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unity-dependency-injection 2 | 3 | ## Motivation 4 | Allow references to high level objects (Typically managers or services), without using singletons or spaghetti serialization, or endless constructor parameters. 5 | 6 | ## How to use it 7 | 1. Create a `DependencyContainer` and fill it with objects you want to inject to other places. 8 | 9 | ```c# 10 | var container = new DependencyContainer(); 11 | container.Add(new PlayerManager()); 12 | container.Add(new BulletManager()); 13 | container.Add(new EnemyManager()); 14 | ``` 15 | 16 | 2. Self inject. This injects each dependency into all the others 17 | 18 | ```c# 19 | container.SelfInject(); 20 | ``` 21 | 22 | 3/ Use `[Inject]` attribute on private fields that you want to be populated by dependencies from the container 23 | ```c# 24 | class PlayerManager 25 | { 26 | // when self inject is called this field will be populated. 27 | [Inject] 28 | private readonly BulletManager bulletManager = null; 29 | 30 | private void HandleOnPlayerFired(Player player) 31 | { 32 | // now we have access to the bullet manager 33 | bulletManager.SpawnBullet(player.Weapon.SpawnPoint); 34 | } 35 | } 36 | ``` 37 | 38 | ## Other uses 39 | 40 | ### Inject to a single object 41 | `container.InjectTo(targetObject)` 42 | 43 | ### Inject to a game object in the scene 44 | `container.InjectToGameObject(gameObject, includeChildObjects)` 45 | 46 | ### Inject to all scene objects 47 | `container.InjectToSceneObjects()` 48 | Note: this will field all mono behaviour types with injectable fields and attempt to find all of them in the scene. This can be an expensive operation depending on how many objects and how many injectable mono behaviours you have. Not recommended during game play *See below 49 | 50 | ### DependencyContainer is also injected 51 | ```c# 52 | [Inject] 53 | private DependencyContainer dependencyContainer = null; 54 | ``` 55 | This can be useful if you want to spawn objects later on and inject to them. 56 | 57 | ### Manually obtain a dependency from the container 58 | ```c# 59 | container.GetDependency(); 60 | container.GetDependency(type); 61 | ``` 62 | 63 | ### Be notified when injection has completed 64 | Implement `IDependencyInjectionCompleteHandler` 65 | 66 | Use this like a constructor or an Awake/Start, and know when your dependencies have been injected 67 | ```c# 68 | class MyInjectionTarget : IDependencyInjectionCompleteHandler 69 | { 70 | void IDependencyInjectionCompleteHandler.HandleDependencyInjectionComplete() 71 | { 72 | // dependencies should now be present. 73 | } 74 | } 75 | ``` 76 | 77 | ### Destroy the container 78 | `container.Destroy()`. 79 | This is just a notification mechanism 80 | If your dependencies need to be notified when the container is destroyed then implement `IDependencyDestructionHandler` and the function `HandleDependenciesDestroyed` will be called on each implementing object in the container 81 | 82 | ## Best practises 83 | 84 | ### Use it in your loading screen. 85 | For best performance it's best to create your dependencies & container and inject everything after scenes have been loaded but before you start the game. 86 | It's also easier to reason about. Sometimes runtime injection is necessary such as injecting to objects that are spawned during the course of a game. This can be circumvented by using pooling, and instantiating and injecting up front. 87 | 88 | ### Inject by a more abstract type. 89 | The type of an injected field doesn't have to be the exact type of the dependency. You can protect parts of your API using more abstracted types. In the best case an interface. 90 | 91 | EG: A BulletManager could do multiple things. But if your weapon only needs to create bullets, don't give it the full API access. 92 | Instead of injection BulletManager inject an interface with a slimmer API. 93 | 94 | ```c# 95 | class BulletManager : IBulletCreationService, IBulletDestructionService, IBulletAccessService {} 96 | 97 | class Weapon 98 | { 99 | [Inject] 100 | private IBulletCreationService bulletCreator = null; 101 | 102 | public void Fire() 103 | { 104 | var bullet = bulletCreator.CreateBullet(); 105 | // I can only create bullets here, I can't destroy them or access all the bullets or anything freaky. 106 | } 107 | } 108 | ```` 109 | 110 | ## Debugging 111 | Set the `EnableLogging` property to `true` to show what is getting injection to with what values. 112 | 113 | -------------------------------------------------------------------------------- /Scripts/DependencyContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace UnityDependencyInjection 9 | { 10 | public interface IDependencyProvider 11 | { 12 | object GetDependency(Type t); 13 | } 14 | 15 | public class DependencyContainer : IDependencyProvider 16 | { 17 | private static readonly Dictionary injectableTypes; 18 | 19 | static DependencyContainer() 20 | { 21 | // build up a cache of all injectable types 22 | injectableTypes = AppDomain.CurrentDomain.GetAssemblies() 23 | // Find all types 24 | .SelectMany(a => a.GetTypes()) 25 | // for each one return a new Injectable or null 26 | .Select(t => 27 | { 28 | // get all fields on this type that have [Inject] Attribute 29 | var injectableFields = InjectableTypeInfo.GetInjectableFields(t); 30 | // if there are none, return null, otherwise create the new injectable object 31 | if (!injectableFields.Any()) return null; 32 | return new InjectableTypeInfo(t, injectableFields); 33 | }) 34 | // filter out non injectable types 35 | .Where(i => i != null) 36 | .ToDictionary(t => t.Type, t => t); 37 | } 38 | 39 | public bool EnableLogging { get; set; } 40 | 41 | private readonly Dictionary dependencies = new Dictionary(); 42 | 43 | public DependencyContainer() 44 | { 45 | Add(this); 46 | } 47 | 48 | public void Add(object obj) 49 | { 50 | if (obj == null) throw new ArgumentNullException(nameof(obj)); 51 | 52 | var type = obj.GetType(); 53 | 54 | if (dependencies.ContainsKey(type)) 55 | { 56 | throw new Exception("Object of this type already exists in the dependency container"); 57 | } 58 | 59 | dependencies.Add(type, obj); 60 | } 61 | 62 | public T GetDependency() where T : class 63 | { 64 | return GetDependency(typeof(T)) as T; 65 | } 66 | 67 | public object GetDependency(Type type) 68 | { 69 | if (dependencies.ContainsKey(type)) 70 | { 71 | return dependencies[type]; 72 | } 73 | 74 | return dependencies.FirstOrDefault(kvp => type.IsAssignableFrom(kvp.Key)).Value; 75 | } 76 | 77 | public IEnumerable GetDependencies() where T : class 78 | { 79 | var result = new List(); 80 | 81 | foreach (var dependency in dependencies) 82 | { 83 | var typedDependency = dependency.Value as T; 84 | if (typedDependency != null) 85 | { 86 | result.Add(typedDependency); 87 | } 88 | } 89 | 90 | return result; 91 | } 92 | 93 | public void SelfInject() 94 | { 95 | if (EnableLogging) 96 | { 97 | Log("Performing SelfInject()"); 98 | } 99 | 100 | foreach (var dependency in dependencies.Values) 101 | { 102 | InjectTo(dependency); 103 | } 104 | } 105 | 106 | public void InjectTo(params object[] targetObjects) 107 | { 108 | if (targetObjects == null) throw new ArgumentNullException(nameof(targetObjects)); 109 | 110 | foreach (var targetObject in targetObjects) 111 | { 112 | if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); 113 | 114 | InjectTo(targetObject); 115 | } 116 | } 117 | 118 | public void InjectTo(object targetObject) 119 | { 120 | if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); 121 | 122 | var targetObjectType = targetObject.GetType(); 123 | 124 | if (!injectableTypes.TryGetValue(targetObjectType, out var injectableType)) return; 125 | 126 | if (EnableLogging) 127 | { 128 | Log($"Performing InjectTo({targetObject})", targetObject); 129 | } 130 | 131 | var injectableFields = injectableType.InjectableFields; 132 | 133 | foreach (var field in injectableFields) 134 | { 135 | var dependency = GetDependency(field.FieldType); 136 | if (dependency == null) 137 | { 138 | LogWarning( 139 | $"Unmet dependency for {targetObjectType.Name}.{field.Name} ({field.FieldType})"); 140 | } 141 | else if (EnableLogging) 142 | { 143 | Log($"Injecting to {targetObjectType}.{field.Name}; Type=({dependency.GetType().Name}) Value={dependency}({field.FieldType})", 144 | dependency); 145 | } 146 | 147 | field.SetValue(targetObject, dependency); 148 | } 149 | 150 | try 151 | { 152 | if (EnableLogging) 153 | { 154 | Log($"Completed injection to {targetObject}", targetObject); 155 | } 156 | var handler = targetObject as IDependencyInjectionCompleteHandler; 157 | handler?.HandleDependencyInjectionComplete(); 158 | } 159 | catch (Exception e) 160 | { 161 | LogError(e.Message); 162 | LogError(e.StackTrace); 163 | } 164 | } 165 | 166 | public void InjectToSceneObjects(bool includeInactiveObjects = true) 167 | { 168 | if (EnableLogging) Log($"Performing InjectToSceneObjects({includeInactiveObjects})"); 169 | 170 | foreach (var injectableTypeInfo in injectableTypes.Values) 171 | { 172 | if (!injectableTypeInfo.IsMonoBehaviour) continue; 173 | 174 | var injectableObjects = Object.FindObjectsOfType(injectableTypeInfo.Type, includeInactiveObjects); 175 | foreach (var injectableObject in injectableObjects) 176 | { 177 | if (injectableTypeInfo.IgnoreSceneInjection) continue; 178 | 179 | // We only want the exact type here, not subclasses 180 | // - since we will also look for them later and we don't want double injections. 181 | // - There's a potential optimization opportunity here: see the end of the function... 182 | if (injectableObject.GetType() != injectableTypeInfo.Type) continue; 183 | 184 | // Special case to stop re-injecting to things in the pool 185 | if (dependencies.Values.Contains(injectableObject)) continue; 186 | 187 | InjectTo(injectableObject); 188 | } 189 | } 190 | 191 | // OPTIMIZATION OPPORTUNITY: 192 | // The algorithm will search the scene for all injectable types, extending from MonoBehaviour. 193 | // If any injectables are inherited, that will result in multiple searches. 194 | // Example: `SubClass : BaseClass` 195 | // even if subclass has no injected fields of it's own, it's included since it has injected fields in the base class. 196 | // The guard in the above function stops us from performing multiple injections 197 | // and we won't injection occur when iterating the BaseClass type. 198 | // We currently only inject for exact type matches. 199 | 200 | // The optimization opportunity: Detect inheritance hierarchies and cache them in the InjectableTypeInfo datastructure. 201 | // When we search for injectables using Object.FindObjectsOfType - we only need to search for the base class 202 | // as this will find any instances of any subclasses. 203 | // When we found those instances we can use the InjectableTypeInfo object of the base type to traverse and find 204 | // the InjectableTypeInfo representing the actual type and use that to inject. This will result in less "Find" calls. 205 | } 206 | 207 | public void InjectToGameObject(GameObject gameObject, bool includeChildren = true) 208 | { 209 | if (EnableLogging) Log($"Performing InjectToGameObject({gameObject}, {includeChildren})", gameObject); 210 | 211 | foreach (var injectable in injectableTypes.Values) 212 | { 213 | // OPTIMIZATION OPPORTUNITY: Same as the above function 214 | var type = injectable.Type; 215 | 216 | var components = includeChildren 217 | ? gameObject.GetComponentsInChildren(type) 218 | : gameObject.GetComponents(type); 219 | 220 | foreach (var injectableObject in components) 221 | { 222 | if (dependencies.Values.Contains(injectableObject)) continue; 223 | 224 | InjectTo(injectableObject); 225 | } 226 | } 227 | } 228 | 229 | public void Destroy() 230 | { 231 | foreach (var dependency in dependencies.Values) 232 | { 233 | var destructionHandler = dependency as IDependencyDestructionHandler; 234 | destructionHandler?.HandleDependenciesDestroyed(); 235 | } 236 | 237 | dependencies.Clear(); 238 | } 239 | 240 | private static void Log(string value, object context = null) => Debug.Log($"[DependencyContainer]: {value}", context as Object); 241 | private static void LogWarning(string value, object context = null) => Debug.LogWarning($"[DependencyContainer]: {value}", context as Object); 242 | private static void LogError(string value, object context = null) => Debug.LogError($"[DependencyContainer]: {value}", context as Object); 243 | 244 | private class InjectableTypeInfo 245 | { 246 | public bool IsMonoBehaviour { get; } 247 | public Type Type { get; } 248 | public IEnumerable InjectableFields { get; } 249 | public bool IgnoreSceneInjection { get; } 250 | 251 | public InjectableTypeInfo(Type type, IEnumerable injectableFields) 252 | { 253 | IsMonoBehaviour = type.IsSubclassOf(typeof(MonoBehaviour)); 254 | IgnoreSceneInjection = type.GetCustomAttribute() != null; 255 | Type = type; 256 | InjectableFields = injectableFields; 257 | } 258 | 259 | public override string ToString() 260 | { 261 | return Type.Name; 262 | } 263 | 264 | public static IEnumerable GetInjectableFields(Type type) 265 | { 266 | const BindingFlags BINDING_FLAGS = BindingFlags.Instance | BindingFlags.NonPublic; 267 | 268 | var fieldInfos = type.GetFields(BINDING_FLAGS).ToList(); 269 | 270 | var currentType = type; 271 | while (currentType.BaseType != typeof(object)) 272 | { 273 | if (currentType.BaseType == null) break; 274 | fieldInfos.AddRange(currentType.BaseType.GetFields(BINDING_FLAGS)); 275 | currentType = currentType.BaseType; 276 | } 277 | 278 | return fieldInfos.Where(f => f.GetCustomAttribute() != null); 279 | } 280 | } 281 | } 282 | } --------------------------------------------------------------------------------