Service Locator Pattern¶
Overview¶
LoL Engine uses a Service Locator instead of scattered singletons. Services register during bootstrap (ImprovedGameInitializer → ConfigurableServiceInitializer) and are retrieved through ServiceLocator.Instance.
Benefits for game projects:
- Access services by interface (
IAudioService,IResourceService, …) - Swap implementations in tests via
Register/ClearAllServices - Clear lifecycle:
Initialize()→LateInitialize()→Shutdown() - Optional services:
TryGet/IsRegisteredwithout throwing
Architecture¶
flowchart TD
IGI[ImprovedGameInitializer] --> CSI[ConfigurableServiceInitializer]
CSI --> SL[ServiceLocator.Instance]
SL --> S1[IEventManager]
SL --> S2[IResourceService]
SL --> S3[IAudioService]
SL --> SN[Your game services via IServiceRegistrar]
Client[Your MonoBehaviours / systems] --> SL
Bootstrap flow:
ImprovedGameInitializerloadsServiceConfigurationand configs.ConfigurableServiceInitializercreates enabled services, callsInitialize(), registers each withServiceLocator.- After all services register,
LateInitialize()runs (safe for cross-service lookups). - Game code (or
ServiceAwaiter) runs once services are ready.
You do not manually register core engine services in normal projects — enable toggles on ServiceConfiguration instead.
Core API¶
Access via ServiceLocator.Instance (singleton; private constructor).
using LoLEngine.Core.ServiceManagement.Service;
// Returns null + logs warning if missing
var audio = ServiceLocator.Instance.Get<IAudioService>();
// Non-throwing lookup
if (ServiceLocator.Instance.TryGet<IResourceService>(out var resources))
{
await resources.LoadAsync<Texture2D>("UI/Icon");
}
// Throws ServiceNotFoundException if missing
var saves = ServiceLocator.Instance.Require<ISaveSystem>();
// Check before optional features
if (ServiceLocator.Instance.IsRegistered<INotificationService>())
{
ServiceLocator.Instance.Get<INotificationService>().Send("Hello", "World");
}
| Method | Behavior |
|---|---|
Register<T>(service, persistent = false, key = "") |
Register implementation (tests / custom bootstrap) |
Get<T>(key = "") |
Returns service or null |
TryGet<T>(out service, key = "") |
Returns bool; no exception |
Require<T>(key = "") |
Returns service or throws ServiceNotFoundException |
IsRegistered<T>(key = "") |
Whether interface is registered |
Unregister<T>(key = "") |
Remove one registration |
ClearServices() |
Clear non-persistent registrations |
ClearAllServices() |
Clear everything (tests / shutdown) |
Keyed registration supports multiple implementations of the same interface when needed.
ILoLEngineService Lifecycle¶
Every engine service implements:
public interface ILoLEngineService
{
void Initialize(); // Called when created; other services may not exist yet
void LateInitialize() { } // After ALL services registered — resolve cross-deps here
void Shutdown(); // Cleanup on engine shutdown
}
Rules:
- Do not resolve other services inside
Initialize()unless injected explicitly. - Use
LateInitialize()for cross-service wiring (see ADR-007 in Architecture). - Game-specific services: implement
IServiceRegistrar(auto-discovered) or add entries toServiceConfigurationCustom Services.
Waiting for Services (MonoBehaviour)¶
using LoLEngine.Core.ServiceManagement.Service;
using LoLEngine.Runtime;
using UnityEngine;
public class MyComponent : MonoBehaviour
{
private IAudioService _audio;
void Start()
{
if (ServiceAwaiter.AreServicesReady())
OnServicesReady();
else
ServiceAwaiter.WaitForServices(this, OnServicesReady, OnTimeout);
}
void OnServicesReady()
{
_audio = ServiceLocator.Instance.Get<IAudioService>();
}
void OnTimeout() => Debug.LogError("Services failed to initialize");
}
Cache service references in fields — avoid Get<T>() every frame.
Registering Game Services¶
Option A — IServiceRegistrar (recommended):
public class WalletServiceRegistrar : IServiceRegistrar
{
public int Priority => 0;
public void RegisterServices(IServiceLocator locator, ServiceConfiguration config)
{
var wallet = new WalletService();
wallet.Initialize();
locator.Register<IWalletService>(wallet);
}
}
Option B — Custom Services list on ServiceConfiguration (interface + implementation type names in inspector).
See Samples~/Scripts/09_GameServiceRegistrarSample.cs.
Testing¶
[SetUp]
public void SetUp()
{
LoLEngine.Tests.Edit.EditModeTestEnvironment.ResetSharedState();
ServiceLocator.Instance.Register<IEventManager>(new MockEventManager());
}
[TearDown]
public void TearDown()
{
ServiceLocator.Instance.ClearAllServices();
}
ServiceLocator.ResetInstance() runs on domain reload via [RuntimeInitializeOnLoadMethod(SubsystemRegistration)].
Thread Safety¶
ServiceLocator uses ReaderWriterLockSlim and a fast-path cache for concurrent reads. Registration/lookup from background threads is supported at the registry level.
Unity caveat: Retrieved services (loading assets, spawning objects, triggering events) still require the main thread for Unity API calls. Do not call LoadAsync results or Instantiate from Task.Run without marshaling back to the main thread.
Best Practices¶
- Program to interfaces —
Get<IAudioService>(), not concrete types. - Cache references in
Start/OnServicesReady, notUpdate. - Unsubscribe from events in
OnDisable/OnDestroy. - Optional services — use
TryGetorIsRegistered; don't assume every toggle is enabled. - Static caches in extensions — invalidate on domain reload (see ADR-002 in Architecture).
- Init order — use
[ServiceDependency]for same-phase ordering (ServiceDependencies).
Common Pitfalls¶
| Pitfall | Fix |
|---|---|
Get<T>() returns null |
Enable service in ServiceConfiguration; wait with ServiceAwaiter |
| Service used before init | Wait for OnServicesReady or LateInitialize |
| Memory leak in tests | Call ClearAllServices() in [TearDown] |
Resolving deps in Initialize() |
Move to LateInitialize() |
| Assuming Input service exists | Input was removed before 1.0 (see CHANGELOG.md) — use Unity Input System in game code |
FAQ¶
Why Service Locator instead of DI? Fits Unity's scene/component workflow and requires minimal boilerplate for Asset Store buyers. Constructor injection is still fine inside your own game assemblies.
When should a service be persistent = true?
When registering manually and the instance must survive ClearServices() across scene loads. Engine bootstrap marks core services persistent automatically where needed.
Can I create a second ServiceLocator?
No — constructor is private. Use ClearAllServices() and re-register for isolated tests.
How do optional dependencies work?
if (ServiceLocator.Instance.IsRegistered<INotificationService>())
{
ServiceLocator.Instance.Get<INotificationService>().Send("Title", "Body");
}
Related Docs¶
- ImprovedGameInitializer — bootstrap entry point
- ServiceDependencies — init order attributes
- Architecture — ADRs including service extensibility
- Events — event bus (
IEventManager,EventStartListening)