A dependency injection framework for Unity. Currently work in progress, all rights reserved.
This readme needs work. Check out the Example Scripts for now!
In Game Development, often, a script might depend on the behaviors of other scripts. For example, a concrete implementation Guard might need a concrete implementation of IGuardManager to find nearby colleagues, so it can alert them of a nearby enemy. The Guard thus depends on GuardManager.
To resolve this dependency on the GuardManager in the Guard class there are generally two approaches in Unity:
- find a reference in the guard itself
- i.e. in the
Awake()function you might call Unity'sGameObject.Find()orgameObject.GetComponent<Foo>
- i.e. in the
- inject this dependency into
Guard, passing the reference to the concrete implementation ofGuardManagerto an instance of Guard.- This can be achieved by various methods, such as method injection or constructor injection.
- i.e.
Guard guard = new Guard(IGuardManager guardManager) { this.guardManager = guardManager; }
The second approach follows the single-responsibility principle (RSP) more closely: The Guard class no longer needs to worry about resolving it's own dependency, so all the code in the Guard class can simply be related to it's behaviour.
Another class then has the responsibility to define the dependency of the type GuardManager , and another class can inject it into the Guard upon creation: This is an example of dependency injection, a design pattern in which an object or function receives other objects or functions that it depends on (instead of resolving them manually in the dependee). This principle is also reffered to as Inversion of Control (IOC).
A DI framework is a codebase that can help you automate the process of dependency injection, and can introduce a more streamlined workflow for developing your game, centralizing the 'needs' that your game has. It can also be very handy during testing: when writing unit tests, you may need to test a class that has multiple dependencies, some of which need to be mocked, and some of which need specific concrete implementations. A dependency injection framework can make it easier to set up these specific tests, and can allow you to re-use the setup process for certain tests. Unify is such a framework for Unity. In this assignment you'll take it upon yourself to try to make use of Unify to apply these principles yourself.
- Clone or fork the project from https://github.com/kemmel-dev/Unify
- Open the 'Example Scene'.
- Check out the objects in the hierarchy of this scene:
- Note the
FooBehaviourthat is attached as a component to a game object in the hierarchy. It extends fromUnifyBehaviour - There are four installers: The
RootInstallerand threeExampleInstallers. Verify that:- the
Example Installerhas theFooBehaviourlinked from the object in the hierarchy, and has astringvalue that is set in the inspector. - the
Example Installer From Prefab With Factoryhas theBarFactoryBehaviourPrefabselected fromResources/Prefabs - the
Example Installer With Interfaceshas no dependencies shown in the inspector. - the
RootInstallerhas a list with references to all threeExampleInstallersabove.
- the
- Note the
- Start the scene. You can click on any of the Behaviour objects to see the relevant GUI-button for that object drawn in it's
OnGUImethod. Double-click on the debug log message to see the relevant code that the message triggered from.- Testing simple behaviours
- Call DoSomething() on the
FooBehaviourThatAlreadyExistsInHierarchyand verify that the behaviour does something on the relevant gameobject. - Call DoSomething() on the
FooBehaviourFromCodeand verify that the behaviour does something on the relevant newly created gameobject.
- Call DoSomething() on the
- Testing behaviours with dependencies
- Call DoSomething() on the
BarBehaviourWithDependencyand verify that:- BarBehaviour prints out the string that is shown in the Example Installer inspector.
- BarBehaviour calls
DoSomething()onFooBehaviourFromCode - BarBehaviour calls
DoSomething()onFooBehaviourThatAlreadyExistsInHierarchy
- Change the string inspector value for the
BarBehaviourWithDependencyand again callDoSomething()on it.- Verify that the debug log now outputs a different string dependency - allowing for test value changes in playmode.
- Call DoSomething() on the
- Testing behaviours with dependencies through interfaces
- Verify that the
BazBehaviourexecutes a methodDoSomethingOnAnInterfaceimplemented from theIBazinterface. - Verify that the
QuxBehaviourexecuted that same method on the sameBazBehaviour, now referenced throughIBaz.
- Verify that the
- Testing behaviours with factories that create other behaviours in runtime.
- Verify that the
BarFactoryBehaviourPrefab(Clone)can instantiate newBarBehaviours(remember to also verify that these instantiated BarBehaviours have their dependencies resolved!) - Verify that it can also spawn new
BarBehaviourswith some custom code running before theBarBehavioursStart function executes. - Finally, verify that it can also spawn new
BarBehaviours that all have unique values for theirstringdependencies.
- Verify that the
- Testing simple behaviours
- Dive into the Installer code!
- Open the
ExampleInstallerand try to understand how the dependencies are defined and then registered.- It defines a dependency of type
stringfrom the instance that is assigned in the inspector. - It also defines a dependency on a
BarBehaviourthat is created in code. - It defines two dependencies of the same type,
FooBehaviour, each with their own unique stringidto be able to differentiate between the two.
- It defines a dependency of type
- Open the
ExampleInstallerWithInterfacesand try to understand how the instance ofBazBehaviouris now referenced through the interfaceIBaz, meaning that if other classes rely onIBazthey will receive that concrete implementation ofBazBehaviour - Open the
ExampleInstallerFromPrefabWithFactoryand try to understand:- How a
BarFactoryBehaviourrelies on an instance of the prefab atPrefabs/BarFactoryBehaviourPrefab. - How we create a new instance of a non-monobehaviour class
BarBehaviourFactorythat will be used in theBarFactoryBehaviour.
- How a
- Open the
- Understanding Factories
- Open the
BarBehaviourFactoryand try to understand how:- We manually resolve the dependencies for the object that is created by this factory using
ResolveFromContainer<> - How we can add a custom override for the default factory method using the
[FactoryOverride(id)]attribute.
- We manually resolve the dependencies for the object that is created by this factory using
- Now open the
BarFactoryBehaviourand try to understand how we use the above factory to:- Create an instance of
BarBehaviourin theCreateAnInstanceOfBar()method. - Create an instance of
BarBehaviourafter which we immediately execute some custom code using theCreateAnInstanceOfBarWithSomeCustomLogicBeforeItsStartFunction()method. - Create a new
DependencyOverrideobject which then passes the objects that need to be passed to the method marked with[FactoryOverride(id)]so that we can alter (part of the required) dependencies during runtime.
- Create an instance of
- Open the
- Understanding tests
- Open the
FooandFooMonoclasses inUsedInExampleTests - Open the
FooMonoTestand try to understand how:- In the
FooMonoTestInstaller...- An automocking substitute (using
NSubstitute) for an implementation of theIFoothatFooMonois defined. - An instance for
FooMonois created and registered as a dependency.
- An automocking substitute (using
- In the
FooMonoTest...- We add the
SubInstallerfor this test - We perform a Unit Test on
FooMono.
- We add the
- In the
- Open the
In this assignment, we will create a factory behaviour that instantiates characters that can wield different types of guns.
Creating the guns:
- Create a new scene and add a
RootInstallerby adding the component to an empty gameobject. - Create an interface
IGunthat has a methodShoot - Create a
PewGun : IGunand aPopGun : IGun, whichDebug.Log()'pew' and 'pop' in theShoot()method respectively.
Creating the character:
- Create a
Character : UnifyBehaviour, which has a dependency on:IGun gunand aVector3 spawnPosition. (To add new behaviour scripts easily, you can useAssets > Create > Unify > Behaviour). Mark the injection method with[Inject] - Create a method in
CharactercalledAttack()which callsgun.Shoot(), and call this method in the update method when a key is pressed. Also set the transform.position to the spawn position in theStart()method. - Create a new gameobject and add the
Characterscript to it.
Trying out the guns:
- Define a dependency of type
IGunin the newMonoInstallerand register it to a concrete implementationPewGun. Also define a dependeny of typeVector3, and assign a custom value to it through the inspector. - Observe whether the character can shoot the PewGun.
- Change, in the dependency definition of
IGun, the concrete implementation ofPewGuntoPopGun. - Observe whether the character can shoot the PopGun.
Creating a factory:
- Remove the Character from the hierarchy.
- Create a
CharacterFactorywhich instantiates objects of typeCharacterand aCharacterFactoryBehaviourwhich depends on aCharacterFactory. A template can be created withAssets > Create > Unify > Behaviour Factory - Throw a
NotImplementedExceptionin the default override, then add a new factory method using[FactoryOverride(id: "WithGun")]that takes in a theCharacterand astring gunId, then manually resolve theIGundependency usinggunId. - Implement an Update method in the
CharacterFactoryBehaviourthat on mouse down left creates a character wielding apewGun, and on mouse right creates a character wielding apopGunby calling_factory.Create(name, new DependencyOverride(...))
Trying out the factory:
- In the installer, alter the dependency definition that registers
IGuntoPopGunto be defined by an idpopGun, and add another definition that registersIGuntoPewGunwith the idpewGun. - Observe whether the corresponding characters are created and whether they shoot the right guns.
Writing a test:
- Finally, create a test for the
Characterbehaviour that tests whether theCharacter.Attackmethod calls theShootmethod on a substitute forIGun. Again, useAssets > Create > Unify > Testto create a template for your test script.