Service Locator
What is it?
Service Locator is a design pattern used to manage services from one central place in an application. In Unity, these services are usually systems that need to be accessed from different parts of the project, such as audio, save, input, score, or analytics systems.
The basic idea is simple: a service registers itself to the Service Locator, and another system that needs that service requests it from the locator instead of creating it directly. This way, the code usually depends on an interface rather than a concrete class.
For example, a class asks for IAudioService instead of directly knowing about AudioService. This allows the same code to work with the real audio system, a fake audio system for testing, or a different implementation later.
At first, Service Locator may look similar to Singleton. However, the important difference is that Singleton usually creates a direct dependency on a concrete class, while Service Locator keeps the dependency more flexible through interfaces. This can create a cleaner structure, especially in Unity projects where scene management, object lifetimes, and testing are important.
When is it used?
Service Locator is useful when multiple systems need access to the same service. For example, if different parts of the game need to play sounds, save data, read player input, or send analytics events, managing these services from one place can make the project easier to control.
It can also be considered a more organized alternative when Singleton usage starts becoming scattered across the project. Instead of making every system directly know about each other, services are accessed through the locator. This reduces direct dependencies between systems.
It is also useful when you want to change a service at runtime. For example, the actual game can use a real AudioService, while the test environment can use a NullAudioService that plays nothing or a MockAudioService that records calls. This allows different implementations to be tested without changing the client code.
However, it should not be used everywhere. In small and isolated systems, it can add unnecessary complexity. Also, if it becomes unclear which services a class depends on, the code can become harder to follow as the project grows.
Animation
Code
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Patterns.Architectural.ServiceLocator
{
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> Services = new();
public static void Register<T>(T service)
{
var type = typeof(T);
if (Services.ContainsKey(type))
{
Debug.LogWarning($"{type.Name} already registered. Overwriting.");
Services[type] = service;
return;
}
Services.Add(type, service);
}
public static T Get<T>()
{
var type = typeof(T);
if (!Services.TryGetValue(type, out var service))
{
throw new Exception($"Service of type {type.Name} is not registered.");
}
return (T)service;
}
public static void Unregister<T>()
{
Services.Remove(typeof(T));
}
public static void Clear()
{
Services.Clear();
}
}
}Advantages and Disadvantages
• Advantages:
- It reduces coupling because classes depend on interfaces instead of concrete services.
- It allows central management of shared services from one place.
- It is useful for testing because mock or null services can be registered.
• Disadvantages:
- The biggest downside is hidden dependencies through
Get<T>()calls. - Missing or wrong registrations usually cause runtime errors instead of compile-time errors.
- If overused, it can turn into a global state container.
Tips
When using Service Locator in Unity, it is a good habit to register services through interfaces rather than concrete classes. For example, working with IAudioService instead of AudioService, or ISaveService instead of SaveService, makes future changes much easier.
The registration order of services is also important. In general, service-providing classes should register themselves in Awake(), while the classes that use those services should retrieve them in Start(). This helps reduce errors where a service is requested before it has been registered.
Scene transitions should be handled carefully. If the locator is kept alive with DontDestroyOnLoad, the same service may be registered again in a new scene. In that case, the old service should be cleared or a warning should be shown when a service is replaced. Otherwise, old references, duplicate registrations, or unexpected errors can occur.
It is also important to unregister services inside OnDestroy(). Otherwise, the locator may still think an object from a destroyed scene is available. This can lead to ghost references and NullReferenceException errors, especially during scene changes.
For performance, instead of calling ServiceLocator.Get<T>() every frame, it is better to retrieve the service once in Start() or after initialization and store it in a field. This avoids repeated dictionary lookups and also makes the code easier to read.
Finally, Service Locator should not be used for everything. For small dependencies that are specific to a scene or used by only a few objects, assigning references through the Inspector or using dependency injection can be cleaner. Service Locator makes more sense for shared systems such as audio, save, input, and telemetry.