A light-weight example of dependency injection using [Provide]
and [Inject]
attributes.
The system relies on theattributes and reflection in .NET System.
ProvideAttribute
Providing dependencies. Any instances marked as[Provide]
attribute, it is expected to be supplied with an instance of dependency.InjectAttribute
Injecting dependencies. Any field or method marked as[Inject]
attribute can expect to have the dependency satisfied by the system.
ServiceA
hasInitialze
method, consums and outputs a message on the console.ServiceB
hasInitialze
method, consums and outputs a message on the console.FactoryA
has a field to cache aServiceA
instance.CreateServiceA()
only creats a new instance if the cache is null.
IDependencyProvider
is a contract for identifying a type. It knows which classes in the system can provide dependencies.
-
ClassA
containsServiceA
and has anInject
methodInit
that accepts and assigns an instance ofServiceA
into the field. -
ClassB
contains bothInjuect
field attribute as forServiceA
andServiceB
, for variaty purposes.FactoryA
followsInject
method attribute by usingInit
method. -
Injector
finding and satisfying dependencies. All instances will be Monobehaviours.Injector
utilizes reflection'sBindingFlags
to find the desired denpendency providers.- To automatically find all dependency providers, take advantage of Unity's
FindMonoBehaviour
method to find all providers marked asIDependencyProvider
Injector
has a dictionary for registering the dependency providers and their return types. Once the providers are found, they can be invoked with optional parameters.- A helper function
IsInjectable
returns any fields marked as[Inject]
. UsingFindMonoBehaviour().Where()
to look for any injectable fields inside a MonoBehaviour object. IsInjectable
usesGetType().GetFeilds()
to find field[Inject]
marks.Any()
andWhere()
(Know more about them) are LINQ extention methods.Any()
return a boolean (wheter or not any items match).Where()
returns a new sequence of items matching the predicate.Inject
method inInjector
finds[Inject]
marks in object fields by usingtype.GetFields()
, resolve the provider type and assigns the instance to the[Inject]
field instance.- Similarly,
Inject
finds method[Inject]
marks by usingtype.GetMethods()
. These methods accept injectables as parameters. The query is using LINQSelect()
to find matching parameter types and return them as an array. - Addtionally, to find injectable properties using `type.GetProperties()'. Injectable properties are referred to as members have getters and setters.
Resovle
helps determine if the provider instance is registered.- For injectable fields use
SetValue()
to assign instances. For injectable methods useInvoke()
to call them. - Set
Injector
class Attribute as[DefaultExecutionOrder(<negative number>)]
will allow injector instantiated before any other objects.
-
Provider
supplies dependencies to the injection system. All instances will be Monobehaviours. Can supply itself as references. -
Environment
provides itself as a dependency.- This goes without letting
Factory
creating new instances. Simply useProvide
Attribute onProvideEnvironmentSystem()
and let it returns itself as an instance. - Best to combine with
Singleton
, that will allow autogenerated instance if one doesn't exist in the project. Initialize()
outputs a message.
- This goes without letting
Singleton
inherits fromMonoBehaviour
. It attempts to find existing object that has the same Component. If not create one and attach a new Component of the same Type.
- Provide an instance through a provider.
class Provider{
[Provide]
public IService ProvideService() {
return new Service();
}
}
- Self Provide
class SelfProvider: IDependencyProvider, ISelfProvider{
[Provide]
public ISelfProvider ProvideSelf(){
return this;
}
}
ISelfProvider
can be marked as [Inject]
in other consumers as fields, method parameters or properties.
- Field Injection
class Consumer{
// A established provider can
[Inject] private IService _service;
}
- Method Injection
class Consumer{
private IService _service;
[Inject]
public void Init(IService service){
_service = service;
}
}
- Property Injection
class Consumer{
[Inject] public IService Service {get; private set; }
}
- Miltiple Injections
class Consumer{
private IService _service;
private IFactory _factory;
[Inject]
public void Init(IService service, IFactory factory){
_service = service;
_factory = factory;
}
}
- Components(MonoBehaviours) that has injectables will appear in
Component/Scripts/DependencyInjection
Injector
component has two buttonsValidate Dependencies
andClear All Injectable Fields
.
Inversion of Control is a way to decouple dependencies of services everytime a MonoBehaviour is trying to access them in the context of Unity. No need to manually link game objects together. Leave the automation process to Service Locator.
ServiceLocator
finds services for both the scene and global useage.- It acts as a singleton for both global and scenes depends on configuration.
- The configurations check if other
ServiceLocator
is already registered in the scene and global, if so it does not create a new one. - For scene level
ServiceLocator
it will go through all root game objects to find one if it's not registered in the container. - Menu Items added under
GameObject/ServiceLocator/
. Both service locator variants can be added to the scene.
Bootstrapper
initializes services onAwake()
.GlobalBootstrapper
andSceneBootstrapper
inherits fromBootstrapper
. Each overridesBootstrap()
to call respective configurations and inistantiate instances onAwake()
.GlobalBootstrapper
has additional configuration for persistent singleton ifdontDestoryOnLoad
is set totrue
;
ServiceManager
Registers and gets services when needed.- Both
Register
overloads go through the service type's registration and type check. - Both
Get
andTryGet
do registration and type check before return specified type of service.
- Both
Interface | Class | Description |
---|---|---|
ILocalization |
MockLocalization |
Mocks a method GetLocalizedWord() that takes a word and get translation of another language. |
ISerializer |
MockSerializer |
Mocks a method Serialize() to serialize objects. |
IAuthentication |
MockAuthentication |
Mocks a method Login() to login user. Note: It's a MonoBehaviour . |
IGameService |
MockGameService |
Mocks a method StartGame() to run the game. |
- Register Services
Inside MainScene
, a Player
object registers for global, scene and self scale services. Registration is done in Awake()
Note: MockAuthentication is a MonoBehaviour. In this case, GameObjectExtension GetOrAdd
is used to instantiate it.
ServiceLocator.Global.Register<IGameService>(_gameService = new MockGameService());
ServiceLocator.ForSceneOf(this).Register<ILocalization>(_localization = new MockLocalization());
ServiceLocator.For(this).Register<ISerializer>(_serializer = new MockSerializer());
ServiceLocator.ForSceneOf(this)
.Register<IAuthentication>(_authentication = gameObject.GetOrAdd<MockAuthentication>());
- Get Services
ServiceLocator.For(this)
.Get(out _serializer)
.Get(out _localization)
.Get(out _gameService)
.Get(out _authentication);
After getting services, we can call any service methods however we want.
- SubScenes do not recognize
ServiceLocator
Scene or global. Might worth looking at if subscene is used in the project. - Loading multiple scenes at the same time
SceneServiceLocator
notice multiple instances, they should only be aware of the ones within the scene.
When creating a new object with multiple attributes, overloading constructor can become cumbersome when more than for parameters need to be configured. Fluent Builder helps creating a new object with more readable code.
A built-in class can access owner class' properties. On top of passing parameters, the builder can attach the component to the game object. So no manual attachment required.
var enemy = new Enemy.Builder()
.WithName("Kobolt")
.WithDamange(10)
.WithSpeed(3f)
.WithHealth(20)
.WithIsBoss(false)
.Build();
No need to call Instantiate()
, it's already instantiated.
With data structures like maps and lists of objects, builder can shorten code lengths by using builder approach.
private Dictionary<string, Data> WeaponDataSet = new WeaponDataBuilder()
.AddSword("sword", 11, 200.0f)
.AddDagger("dagger", 13, 400.0f)
.AddBow("bow", 10, 100.0f)
.Build();
Initializing a dinctionary of weapon data is more compact comparing to adding entries one by one.
Unity MonoBehaviour
are attached to GameObjects
as components. Component builder can streamline the process of adding components.
var enemyWithComponents = new Enemy.ComponentBuilder()
.AddArmorComponent()
.AddWeaponComponent(weaponData)
.AddHealthComponent()
.Build();
Data-oriented objects can pass in as parameters for component configurations.
Interfaces enforce contracts, it's useful when component are constructed in a sequence. Each builder method returns the next return interface forces the builder call methods in a certain order.
When having behaviour asssociated with a value, observer pattern can come in handy updating the value across the board. Such as losing health on the player, an Observer updates the health value to the UI when changed.
An example: by hitting a button, player gain 10 points of energy.
- On
Player
public class Player : MonoBehaviour{
[SerializeField] public Observer<int> Energy = new();
private InputAction _clickAction;
private void Start(){
Energy.Invoke();
}
private void Enable(){
var input = new PlayerController();
_clickAction = input.Mouse.Click;
_clickAction.Enable();
_clickAction += _ => { Energy.Value += 10;}
}
private void Disable(){
_clickAction.Disable();
}
}
- On
Display
public class Display : MonoBehaviour{
TMP_Text energyLabel;
private void Awake() {
energyLabel = GetComponent<TMP_Text>();
}
public void UpdateEnergyDisplay(int energy){
energyLabel.text = $"Energy: {energy}";
}
}
Afterward, go to the inspector and subscribe UpdateEnergyDisplay
to Player
component Energy
section, open the drop down to add the function to the event.
Note: it can also be used for custom in-editor tools as well.
Reusable commands can be entities of their own. Commands can be abstracted and self-sufficient to provide generic instances.
C# has introduced interface new features:
- Default method implementations. It can reduce abstract class overhead. Not need to implement defaults in abstract classes with the interface.
- Inner class with the same interface can be used to represent default or null object instances.
Previously Singleton was used in Dependency Injection examples. Singleton pattern makes sure a single instance exists across the software life cycle. In Unity use cases, singleton can only exists as long as one scene exists, or persists as long as the game is running. More complicated than that singleton can self-updating by assign new updated instances to itself.
It inherits from MonoBehaviour
and takes generics that are Component
. Check if the instance doesn't exist, create a new one. And initialize the instance upon Awake()
.
It is the same logic as Regular Singleton. The only difference is that if a GameObject
persists throughout multiple scenes, it should be located in the root level and no parent GameObject
. transform.SetParent(null)
unparent the GameObject
. Upon Awake()
after assigning the instance to self, add DontDestroyOnLoad(gameObject)
.
Regulator Singleton records the time an instance is created and assign the newest one to itself, at the same time destroy the older instances. It manages itself so no need to expose time related properties.
Very simple. Replace MonoBehaviour
with Singleton<>
like:
public class ForeverAlone : MonoBehaviour
{
// Class implementations
}
public class ForeverAlone : Singleton<ForeverAlone>{
// Class implementations
}
Unity Object
and its inheritance classes can have extension methods to expand the generalized functionality not implemented officially by Unity Engine developers.
Disable all children inside the hierarchy of a game object but keep the parrent active.
- Definition
using UnityEngine;
public static class GameObjectExtensions{
public static void DisableChildren(this GameObject gameObject) {
for(var i=0; i<gameObject.transform.childCount; i++){
gameObject.transform.GetChild(i).SetActive(false);
}
}
}
- Usage in the test scene
public class DisableChildrenTest : MonoBehaviour
{
void Start()
{
GameObject parent = new(name: "Parent");
for (var i = 0; i < 5; i++)
{
GameObject child = new($"Child{i}");
child.transform.parent = parent.transform;
}
parent.DisableChildren();
}
}
While in play mode a GameObject named Parent
is spawned and all it's children are disabled in the inspector.