3 min read

Implementing Service Locator in Unity

Implementing Service Locator in Unity
Photo by 愚木混株 cdd20 / Unsplash

Content

Disclaimer

I believe the dev community has differing opinions about Service Locator. Some says it's anti-pattern because it hides class dependencies. In my case, I just think it's awesome and it's a straight forward application of IoC so I decided to implement it.

I might be shooting myself in the foot here but I think it's okay; I can always treat this as a learning experience. Also, I just want a simple implementation of IoC as an attempt to follow S.O.L.I.D. IoC containers are just too complicated for me; especially when trying to implement it in Unity.

Anyway with that aside, let's get to it!

Creating ServiceLocator Singleton

Singleton, another design pattern that is frowned upon by some developers because it's easy to implement and therefore, easy to misuse. It is a powerful super global variable that usually causes spaghetti code.

I know that for a fact because I had experience working on a project with tons of singleton. But with service locator, I believe I can tame this pattern. In fact, I think the only singleton that I need for this project will be the ServiceLocator.

Before going through the implementation, let us first look at the overview of the core functionalities of this class using the IServiceLocator interface.

public interface IServiceLocator 
{
    static IServiceLocator Instance { get; }
    void Register<TService>(TService serviceInstance);
    TService GetService<TService>();
}
Instance singleton reference
Register creates a reference to a service instance using its type as key
GetService returns a reference to a service instance specified by its type

IServiceLocator.Instance

This is a basic implementation of a singleton using the Lazy<T> keyword.

using System;
using System.Collections.Generic;

public sealed class ServiceLocator : IServiceLocator
{
    /* ... */
    
    private ServiceLocator() { }

    private static readonly Lazy<IServiceLocator> _instance = new Lazy<IServiceLocator>(() =>
    {
        return new ServiceLocator();
    });

    public static IServiceLocator Instance
    {
        get
        {
            return _instance.Value;
        }
    }
    
    /* ... */
}

IServiceLocator.Register<TService>(TService serviceInstance)

Here I created a dictionary to hold the service instances. It is important to note that latter registrations of the same type will overwrite the previous ones.

using System;
using System.Collections.Generic;

public sealed class ServiceLocator : IServiceLocator
{
	/* ... */
    
    private readonly Dictionary<Type, object> _serviceRegistry = new Dictionary<Type, object>();

	/* ... */

    public void Register<TService>(TService serviceInstance)
    {
        Type serviceType = typeof(TService);

        if (_serviceRegistry.ContainsKey(serviceType))
        {
            _serviceRegistry[serviceType] = serviceInstance;
            return;
        }

        _serviceRegistry.Add(serviceType, serviceInstance);
    }
}

IServiceLocator.GetService<TService>()

Finally to get a reference of the service instance, we just need to call this function and specify the type.

using System;
using System.Collections.Generic;

public sealed class ServiceLocator : IServiceLocator
{
	/* ... */
    
    private readonly Dictionary<Type, object> _serviceRegistry = new Dictionary<Type, object>();
    
    /* ... */
    
    public TService GetService<TService>()
    {
        Type serviceType = typeof(TService);
        if (_serviceRegistry.ContainsKey(serviceType))
        {
            return (TService)_serviceRegistry[serviceType];
        }

        throw new Exception($"{serviceType.ToString()} is unregistered.");
    }
    
    /* ... */
}

Creating the GameLoader

Now that we have a ServiceLocator, we need a way to register all necessary services when the game loads. That will be the purpose of our GameLoader.

I made GameLoader a MonoBehaviour to take advantage to Unity's event functions; specifically the Awake event. My plan is to use Awake solely to initialize variables and then use Start to call Setup or Create functions of those variables; more on that later.

using UnityEngine;

public class GameLoader : MonoBehaviour
{
    private void Awake()
    {
        IServiceLocator serviceLocator = ServiceLocator.Instance;
        serviceLocator.Register<IWarriorPoolService>(new WarriorPoolService());
        
        /* ... */
    }
}

Going back to the GameLoader, I will register all services in its Awake event. In order to make sure that all services are registered before accessing them, I also set the GameLoader execution order before the default time.

Project Settings > Script Execution Order

This means that calling IServiceLocator.Instance.GetService to initialize a service variable in another MonoBehaviour.Awake call will not cause a null reference exception.

Sample Usage

Earlier I mentioned how I intend to use the Awake and Start events. As an example, here is my implementation of WarriorPoolController.

public class WarriorPoolController : MonoBehaviour
{
    [SerializeField] private WarriorController _warriorPrefab;

    private IWarriorPoolService _warriorPoolService;

    private void Awake()
    {
    	_warriorPoolService = ServiceLocator.Instance.GetService<IWarriorPoolService>();
    }

    private void Start()
    {
    	_warriorPoolService.InitPool(_warriorPrefab, transform);
    }
}

In Awake, I initialized _warriorPoolService. Then in Start, I called the method to begin warrior object pool creation.

Summary

To recap, I decided to implemented Service Locator mainly because, for me, it feels more natural to integrate it in Unity compared to using an IoC container. Then I utilized Unity's events and script execution to properly time the registration and initialization of my services. Lastly, I gave an example how to put them all together.


Thank you for reading! For updates, follow me on Mastodon. You can also subscribe now and leave a comment below!