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 ofGuardManager
to 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
FooBehaviour
that is attached as a component to a game object in the hierarchy. It extends fromUnifyBehaviour
- There are four installers: The
RootInstaller
and threeExampleInstaller
s. Verify that:- the
Example Installer
has theFooBehaviour
linked from the object in the hierarchy, and has astring
value that is set in the inspector. - the
Example Installer From Prefab With Factory
has theBarFactoryBehaviourPrefab
selected fromResources/Prefabs
- the
Example Installer With Interfaces
has no dependencies shown in the inspector. - the
RootInstaller
has a list with references to all threeExampleInstallers
above.
- 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
OnGUI
method. Double-click on the debug log message to see the relevant code that the message triggered from.- Testing simple behaviours
- Call DoSomething() on the
FooBehaviourThatAlreadyExistsInHierarchy
and verify that the behaviour does something on the relevant gameobject. - Call DoSomething() on the
FooBehaviourFromCode
and verify that the behaviour does something on the relevant newly created gameobject.
- Call DoSomething() on the
- Testing behaviours with dependencies
- Call DoSomething() on the
BarBehaviourWithDependency
and 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
BarBehaviourWithDependency
and 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
BazBehaviour
executes a methodDoSomethingOnAnInterface
implemented from theIBaz
interface. - Verify that the
QuxBehaviour
executed 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
BarBehaviours
with some custom code running before theBarBehaviours
Start function executes. - Finally, verify that it can also spawn new
BarBehaviour
s that all have unique values for theirstring
dependencies.
- Verify that the
- Testing simple behaviours
- Dive into the Installer code!
- Open the
ExampleInstaller
and try to understand how the dependencies are defined and then registered.- It defines a dependency of type
string
from the instance that is assigned in the inspector. - It also defines a dependency on a
BarBehaviour
that is created in code. - It defines two dependencies of the same type,
FooBehaviour
, each with their own unique stringid
to be able to differentiate between the two.
- It defines a dependency of type
- Open the
ExampleInstallerWithInterfaces
and try to understand how the instance ofBazBehaviour
is now referenced through the interfaceIBaz
, meaning that if other classes rely onIBaz
they will receive that concrete implementation ofBazBehaviour
- Open the
ExampleInstallerFromPrefabWithFactory
and try to understand:- How a
BarFactoryBehaviour
relies on an instance of the prefab atPrefabs/BarFactoryBehaviourPrefab
. - How we create a new instance of a non-monobehaviour class
BarBehaviourFactory
that will be used in theBarFactoryBehaviour
.
- How a
- Open the
- Understanding Factories
- Open the
BarBehaviourFactory
and 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
BarFactoryBehaviour
and try to understand how we use the above factory to:- Create an instance of
BarBehaviour
in theCreateAnInstanceOfBar()
method. - Create an instance of
BarBehaviour
after which we immediately execute some custom code using theCreateAnInstanceOfBarWithSomeCustomLogicBeforeItsStartFunction()
method. - Create a new
DependencyOverride
object 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
Foo
andFooMono
classes inUsedInExampleTests
- Open the
FooMonoTest
and try to understand how:- In the
FooMonoTestInstaller
...- An automocking substitute (using
NSubstitute
) for an implementation of theIFoo
thatFooMono
is defined. - An instance for
FooMono
is created and registered as a dependency.
- An automocking substitute (using
- In the
FooMonoTest
...- We add the
SubInstaller
for 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
RootInstaller
by adding the component to an empty gameobject. - Create an interface
IGun
that has a methodShoot
- Create a
PewGun : IGun
and aPopGun : IGun
, whichDebug.Log()
'pew' and 'pop' in theShoot()
method respectively.
Creating the character:
- Create a
Character : UnifyBehaviour
, which has a dependency on:IGun gun
and aVector3 spawnPosition
. (To add new behaviour scripts easily, you can useAssets > Create > Unify > Behaviour
). Mark the injection method with[Inject]
- Create a method in
Character
calledAttack()
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
Character
script to it.
Trying out the guns:
- Define a dependency of type
IGun
in the newMonoInstaller
and 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 ofPewGun
toPopGun
. - Observe whether the character can shoot the PopGun.
Creating a factory:
- Remove the Character from the hierarchy.
- Create a
CharacterFactory
which instantiates objects of typeCharacter
and aCharacterFactoryBehaviour
which depends on aCharacterFactory
. A template can be created withAssets > Create > Unify > Behaviour Factory
- Throw a
NotImplementedException
in the default override, then add a new factory method using[FactoryOverride(id: "WithGun")]
that takes in a theCharacter
and astring gunId
, then manually resolve theIGun
dependency usinggunId
. - Implement an Update method in the
CharacterFactoryBehaviour
that on mouse down left creates a character wielding apewGun
, and on mouse right creates a character wielding apopGun
by calling_factory.Create(name, new DependencyOverride(...))
Trying out the factory:
- In the installer, alter the dependency definition that registers
IGun
toPopGun
to be defined by an idpopGun
, and add another definition that registersIGun
toPewGun
with 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
Character
behaviour that tests whether theCharacter.Attack
method calls theShoot
method on a substitute forIGun
. Again, useAssets > Create > Unify > Test
to create a template for your test script.