A lightweight service locator pattern implementation for Unity, designed to give you full control over service initialization order, lifetime, and access across your project.
- Register and resolve services by type at runtime
- Automatic dependency ordering via
[ServiceImplementation(DependsOn = ...)] ServiceReference<T>for lazy, cached access with lifecycle eventsServicesReporterBasefor grouping service registration by scene/context- Optional code generation for static access to services
- Conditional service registration via
IConditionalService - Lifecycle callbacks via
IOnServiceRegistered/IOnServiceUnregistered - UniTask support for async service waiting
Any class can be a service. Use [ServiceImplementation] to enable code generation and declare dependencies:
[ServiceImplementation(DependsOn = new[] { typeof(SettingsService) })]
public class AudioService : MonoBehaviour, IOnServiceRegistered, IOnServiceUnregistered
{
private readonly ServiceReference<SettingsService> _settingsService = new();
void IOnServiceRegistered.OnRegisteredOnServiceLocator(ServiceLocator serviceLocator)
{
// Safe to access dependencies here - they are guaranteed to be registered
_settingsService.Reference.Settings.Audio.MasterVolume.OnChangedEvent += OnMasterVolumeChanged;
}
void IOnServiceUnregistered.OnUnregisteredFromServiceLocator(ServiceLocator serviceLocator)
{
if (_settingsService.HasCachedReference)
{
_settingsService.Reference.Settings.Audio.MasterVolume.OnChangedEvent -= OnMasterVolumeChanged;
}
}
}Create ServicesReporterBase subclasses to group services by context (bootstrap, gameplay, lobby, etc.). The reporter registers on Awake and unregisters on OnDestroy, tying service lifetime to the scene.
public class BootstrapServiceReporter : ServicesReporterBase
{
[SerializeField] private AudioService _audioService;
[SerializeField] private SettingsService _settingsService;
[SerializeField] private InputService _inputService;
protected override void RegisterServices()
{
ServiceLocator.Instance.RegisterInstance(_settingsService);
ServiceLocator.Instance.RegisterInstance(_audioService); // waits for SettingsService via DependsOn
ServiceLocator.Instance.RegisterInstance(_inputService);
}
protected override void UnregisterServices()
{
ServiceLocator.Instance.UnregisterInstance(_audioService);
ServiceLocator.Instance.UnregisterInstance(_settingsService);
ServiceLocator.Instance.UnregisterInstance(_inputService);
}
}Scene-scoped reporters let you register services that only live during a specific game state:
public class LobbyServiceReporter : ServicesReporterBase
{
[SerializeField] private CameraService _cameraService;
[SerializeField] private LobbyService _lobbyService;
protected override void RegisterServices()
{
ServiceLocator.Instance.RegisterInstance(_cameraService);
ServiceLocator.Instance.RegisterInstance(_lobbyService);
}
protected override void UnregisterServices()
{
ServiceLocator.Instance.UnregisterInstance(_cameraService);
ServiceLocator.Instance.UnregisterInstance(_lobbyService);
}
}ServiceReference<T> is the primary way to access services. It provides lazy resolution, caching, and automatic cache invalidation when services are registered/unregistered:
public class InGameMenuController : MonoBehaviour
{
private readonly ServiceReference<GameplayService> _gameplayService = new();
private readonly ServiceReference<SessionService> _sessionService = new();
private void OnEnable()
{
// .Reference resolves and caches the service on first access
_lobbyButton.gameObject.SetActive(_sessionService.Reference.IsHost);
}
}// Check if the service is registered (does not cache)
if (_audioService.Exists) { ... }
// Check if the service is registered AND we have a valid cached reference
if (_audioService.HasCachedReference) { ... }// One-shot callback: fires immediately if already registered, then unsubscribes
_audioService.WhenServiceBecomesAvailable(() =>
{
// Service is now available
});
// Persistent events: fires on every register/unregister
_audioService.OnWhenServiceGetsRegistered += OnAudioAvailable;
_audioService.OnWhenServiceGetsUnregistered += OnAudioRemoved;private IEnumerator Start()
{
yield return _audioService.WaitForServiceBeAvailableEnumerator();
// AudioService is now available
}Requires the UNITASK_ENABLED scripting define:
private async UniTask InitializeAsync(CancellationToken token)
{
await _audioService.WaitForServiceBeAvailableAsync();
// or directly:
await ServiceLocator.Instance.WaitForServiceAsync<AudioService>(token);
}For cases where you know the service is registered:
var audio = ServiceLocator.Instance.GetInstance<AudioService>();
// Safe variant
if (ServiceLocator.Instance.TryGetInstance<AudioService>(out var audio))
{
audio.PlaySound(...);
}You can register a concrete type under an interface or base class, allowing consumers to depend on abstractions:
// Register the concrete type under its base class
ServiceLocator.Instance.RegisterInstance<PhysicsService>(networkedPhysicsService);
// Consumers only know about the base type
private readonly ServiceReference<PhysicsService> _physics = new();Services decorated with [ServiceImplementation(DependsOn = ...)] are automatically held in a waiting queue until all their dependencies are registered. This means you can register services in any order and the locator will resolve them correctly:
[ServiceImplementation(DependsOn = new[] { typeof(SaveDataService), typeof(GraphicsService) })]
public class SettingsService : MonoBehaviour { ... }If SettingsService is registered before SaveDataService, it will be queued and automatically registered once SaveDataService becomes available.
| Interface | When it fires |
|---|---|
IOnServiceRegistered |
Immediately after the service is added to the locator |
IOnServiceUnregistered |
Immediately before the service is removed from the locator |
IConditionalService |
Called during registration to decide if the service should be registered at all |
Classes with [ServiceImplementation] are picked up by the code generator, creating a static file for direct access:
[ServiceImplementation(Category = "Game")]
public class GameplayService : MonoBehaviour { ... }
// Generated static access:
Services.Game.Gameplay // returns the service instance
Services.Game.Ref.Gameplay // returns the ServiceReference<T>Configure code generation in Project Settings > Service Locator.
Add from OpenUPM | via scoped registry, recommended
This package is available on OpenUPM: https://openupm.com/packages/com.brunomikoski.servicelocator
To add the package to your project:
- open
Edit/Project Settings/Package Manager - add a new Scoped Registry:
Name: OpenUPM URL: https://package.openupm.com/ Scope(s): com.brunomikoski - click Save
- open Package Manager
- click +
- select Add from Git URL
- paste
com.brunomikoski.servicelocator - click Add
Add from GitHub
- open Package Manager
- click +
- select Add from Git URL
- paste
https://github.com/brunomikoski/ServiceLocator.git - click Add