What Is It?
A Service Locator is a central registry that maps a type to an instance. At startup a bootstrap script calls ServiceLocator.Register<IAudioService>(new UnityAudioService()); later, any script that wants to play a sound calls ServiceLocator.Get<IAudioService>().Play("jump").
Callers depend on the interface and on the locator, but not on the concrete class or on where it lives in the scene. That removes the scattered FindObjectOfType calls and hard singletons that otherwise creep into a Unity project.
A good locator also has a plan for missing services. Returning a Null Object — a NullAudioService whose methods do nothing — lets a test scene with no audio setup run happily instead of throwing a NullReferenceException in every enemy's Start().
When Is It Used?
Use it for truly global, cross-cutting services: audio, analytics, save/load, input, localisation — things nearly every system touches.
It is a pragmatic step up from singletons in projects that are not ready for a DI framework: you get swappable implementations with almost no setup.
Avoid it for gameplay relationships (a turret and its target, a door and its key). Those should be explicit references, not global lookups hidden inside method bodies.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Architectural.ServiceLocator
{
/// <summary>
/// A typical client. It asks the locator for services by interface and
/// never cares which implementation answers.
/// </summary>
public class EnemyClient : MonoBehaviour
{
private IAudioService audio;
private IAnalyticsService analytics;
private void Start()
{
// Cache lookups once instead of resolving every frame.
audio = ServiceLocator.Get<IAudioService>();
analytics = ServiceLocator.Get<IAnalyticsService>();
}
public void Die()
{
audio.Play("enemy_death");
analytics.Track("enemy_killed", name);
Destroy(gameObject);
}
}
}using System;
using System.Collections.Generic;
using UnityEngine;
namespace Patterns.Architectural.ServiceLocator
{
/// <summary>
/// A static type-to-instance registry. Missing services fall back to a
/// registered Null Object when one exists.
/// </summary>
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> services = new();
private static readonly Dictionary<Type, object> fallbacks = new();
public static void Register<T>(T service) where T : class =>
services[typeof(T)] = service ?? throw new ArgumentNullException(nameof(service));
public static void Unregister<T>() where T : class => services.Remove(typeof(T));
public static void RegisterFallback<T>(T nullService) where T : class =>
fallbacks[typeof(T)] = nullService;
public static T Get<T>() where T : class
{
if (services.TryGetValue(typeof(T), out var service))
return (T)service;
if (fallbacks.TryGetValue(typeof(T), out var fallback))
{
Debug.LogWarning($"ServiceLocator: {typeof(T).Name} not registered, using fallback.");
return (T)fallback;
}
throw new InvalidOperationException($"ServiceLocator: no service for {typeof(T).Name}.");
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
services.Clear();
fallbacks.Clear();
}
}
}using UnityEngine;
namespace Patterns.Architectural.ServiceLocator
{
/// <summary>Plays named sound effects.</summary>
public interface IAudioService
{
void Play(string clipId);
}
/// <summary>Records gameplay events for analytics.</summary>
public interface IAnalyticsService
{
void Track(string eventName, string detail);
}
/// <summary>Real audio implementation.</summary>
public class UnityAudioService : IAudioService
{
private readonly AudioSource source;
public UnityAudioService(AudioSource source) => this.source = source;
public void Play(string clipId)
{
var clip = Resources.Load<AudioClip>($"Audio/{clipId}");
if (clip != null) source.PlayOneShot(clip);
}
}
/// <summary>Null Object: safe to call, does nothing.</summary>
public class NullAudioService : IAudioService
{
public void Play(string clipId) { }
}
/// <summary>Debug analytics that just prints.</summary>
public class ConsoleAnalytics : IAnalyticsService
{
public void Track(string eventName, string detail) =>
Debug.Log($"[Analytics] {eventName}: {detail}");
}
/// <summary>Null Object for analytics (e.g. user opted out).</summary>
public class NullAnalytics : IAnalyticsService
{
public void Track(string eventName, string detail) { }
}
}using UnityEngine;
namespace Patterns.Architectural.ServiceLocator
{
/// <summary>
/// Registers services before any scene loads, plus Null Object
/// fallbacks so optional services never cause null references.
/// </summary>
public static class GameBootstrap
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Init()
{
ServiceLocator.RegisterFallback<IAudioService>(new NullAudioService());
ServiceLocator.RegisterFallback<IAnalyticsService>(new NullAnalytics());
var audioHost = new GameObject("[Audio]");
Object.DontDestroyOnLoad(audioHost);
var source = audioHost.AddComponent<AudioSource>();
ServiceLocator.Register<IAudioService>(new UnityAudioService(source));
#if DEVELOPMENT_BUILD || UNITY_EDITOR
ServiceLocator.Register<IAnalyticsService>(new ConsoleAnalytics());
#endif
}
/// <summary>Example of a runtime swap, e.g. from a settings menu.</summary>
public static void MuteAll() =>
ServiceLocator.Register<IAudioService>(new NullAudioService());
}
}Advantages & Disadvantages
+ Advantages
- Swapping an implementation at runtime is one
Registercall — great for debug overlays and tests. - Far less wiring than DI: no installers, no constructor plumbing through layers.
- Pairs naturally with Null Objects, so optional services fail silently and safely.
− Disadvantages
- Dependencies are hidden inside method bodies; you cannot tell what a class needs from its signature.
- It is global mutable state, so registration order and scene reloads can cause subtle bugs.
- Overuse turns it into a junk drawer that every class reaches into, recreating the coupling it was meant to remove.
Tips
- 01Register services in a bootstrap with
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]so they exist before anyAwake. - 02Clear the registry on domain reload (
SubsystemRegistration) when Enter Play Mode Options skip domain reloads, or stale services survive between runs. - 03Cache the result of
Get<T>()inStartfor hot paths rather than looking it up every frame. - 04Log a warning the first time a Null Object is handed out, so missing registrations are noticed instead of silently ignored.
- 05If you find yourself wanting scoped or per-scene services, that is the signal to graduate to Dependency Injection.