A lightweight Service Locator implementation for Unity with scoping support.
- Open the Package Manager
- Click "+" button at the top left
- Select "Add package from git URL" and paste following URL:
https://github.com/bmaczak/ServiceContainer.git
When developing larger games or software with Unity it is not always straightforward how to handle dependencies between different parts of the codebase. Some of the most common solutions are:
- Singletons
- Service Locators
- Dependency Injection
Singletons are very easy to set up and use, however as the instances are stored in the concrete class it makes the code very rigid making testing difficult. And I'm not only talking about unit testing (in my experience not so common for games), but also test scenes or scenarios where some systems are replaced with a special or mock implementation, which I personally use a lot.
Service Locators give more flexibility as you can ask for interfaces or abstract classes without caring what implementation you are getting back, however dependencies for a system are not clear at a glance and the lifecycle and ownership of the created services might cause problems (I'm aiming to solve the latter).
Dependency Injection solves the issues of Service Locators but might be tricky to use with Unity (constructor injection is not feasible for Unity objects), and can add considerable overhead for both development time and performance.
I personally found the Service Locators to be good middle ground for most of the projects I worked on.
The naive implementation usually consists of a static ServiceLocator class that stores the registered services in a Dictionary, and then the MonoBehaviours that implement a service register and unregister themselves in Awake / OnDestroy. However there are some issues:
- How do you register services that are not MonoBehaviours? When do you unregister them?
- How can you create services that exist throughout the whole game lifecycle?
- If a service depends on another service how do you ensure that the dependency is registered in time?
For these problems I use the class ServiceContainer. You can think of the ServiceContainer as a collection of services with the same lifecycle. Services in a container are installed via installers (classes that implement the IServiceInstaller interface). The package contains two classes for creating containers:
- The SceneServiceContainer class holds a container with a lifecycle matches the containing scenes lifecycle
- The GlobalServiceContainer class holds a container that exists throughout the whole game
Both of these classes are MonoBehaviours that get use the IServiceInstaller implementations attached to the same GameObject.
- Create a new MonoBehaviour script
- Make it implement IServiceInstaller
- Implement the InstallServices(ServiceContainer) method.
Inside the method you can install any object. You can have a serialized field in the inspector for a MonoBehaviour or ScriptableObject, you can create new instances, or even choose different implementations based on some conditions.
This is useful when you want some services to exist for a single scene.
- Create an empty GameObject in the scene
- Add the component SceneServiceContainer to it
- Add any installers you need
This is useful for services that need to be available for all of the application lifecycle.
- Create a prefab in the Assets/Resources folder called "GlobalServices".
- Add the component GlobalServicesContainer to it
- Add any installers you need
The GlobalServiceContainer uses the RuntimeInitializeOnLoadMethod attribute, this ensures that the global services will be installed before any scene service.
Often you need to do some initialization on the services you install. If your service is a MonoBehaviour in the scene you can use the normal lifecycle functions like Awake and Start. This is possible because the ServiceContainer installs the services before other Awake functions are called. For non-MonoBehaviour services you can use the IInitializable interface. After installing all services, the container calls Initialize() for all of them that implement the interface. This means that if you have services A and B installed in the same container you can use ServiceLocator.Get<B>() inside A.Initialize().
Similarly before uninstalling services the ServiceContainer goes through all of them and callses Dispose() on all services that implement the IDisposable interface.
A simple sample on how to use the package is available via the Package Manager.