Service Locator

The one static class you call to get any framework or game service.

The one class you call to get any service · Global registry for all framework and game services · Always initialized — no replacement needed

What it does

The Service Locator is a small static registry that answers one question: "where is the X service?" Bootstrap registers each of the 23 framework services into it at startup. Your code calls ServiceLocator.Resolve<IMyService>() from anywhere and gets the instance back — no scene references, no singletons, no compile-time wiring. If you want to swap an implementation (for example, your own save system instead of the default), call Replace<T>() once; nothing else has to change.

Quick example

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]  // Bootstrap registers services first — class-level attribute
public class GameController : MonoBehaviour
{
    [SerializeField] private AudioClip _clickSound;

    // Cache in a field; never call Resolve in Update().
    private IAudioService _audio;
    private ISaveService _save;

    void Awake()
    {
        // Bootstrap has already called ServiceLocator.Register<T>() for each service.
        _audio = ServiceLocator.Resolve<IAudioService>();
        _save = ServiceLocator.Resolve<ISaveService>();
    }

    public void OnPlayButtonPressed()
    {
        _audio.PlaySfx(_clickSound);
    }
}

If you want to run without a real save system — for a test, or to switch the feature off — swap in the built-in no-op implementation:

// In a test setup, or anywhere before the code under test resolves it:
ServiceLocator.Replace<ISaveService>(new NullSaveService());

// New Resolve calls now return the replacement.
// Code that cached the old instance earlier keeps using that old instance —
// the locator never updates references you already hold.

Full API surface

All methods are static on the ServiceLocator class. All of them are main-thread only (see Behavior & edge cases).

Core operations

  • void Register<T>(T instance) — Add a new service under the key T. Throws ServiceAlreadyRegisteredException if a service is already registered for T, and ArgumentException if T is a concrete class (keys must be interfaces or abstract classes). Use this for your own game services; the framework's own services are registered by Bootstrap.

  • T Resolve<T>() — Get the registered service. Throws ServiceNotRegisteredException if nothing is registered for T. This is a dictionary lookup on every call — do not call it every frame; resolve once in Awake/Start and cache the result in a field.

  • void Replace<T>(T instance) — Overwrite or create a registration (no error if one already exists). It does not dispose the instance it displaces — it cannot know whether you still hold a reference. If the outgoing service owns an internal GameObject (Time, Object Pool, Audio, UI Framework, Scheduler, Tween, and Tween Sequencing each create a [CGS] … GameObject), dispose it yourself or that GameObject stays alive and keeps ticking for the rest of the session:

    var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable;
    ServiceLocator.Replace<ITimeService>(new NullTimeService());
    previous?.Dispose();   // after the swap, so nothing resolves a disposed instance
    
  • bool TryResolve<T>(out T service) — Soft lookup; returns false and sets service to null if nothing is registered. No exception. Use it for optional services.

  • bool IsRegistered<T>() — Check existence without fetching the instance.

  • void Unregister<T>() — Remove a registration. Safe to call when nothing is registered (calling it twice is fine).

Debug and test helpers

  • void Reset() — Clear all registrations. Available in the Editor and Development builds only; compiled out of Release builds, so it is not callable in production code paths.
  • IReadOnlyDictionary<Type, object> GetRegistrySnapshot() — Read-only copy of all registered services. Editor only.
  • int RegisteredServiceCount — Live count of registered entries. Editor only.

Exceptions

  • ServiceLocatorException — Base type for all Service Locator errors; catch this to handle any of them.
  • ServiceAlreadyRegisteredExceptionRegister failed because the key exists; exposes a .ServiceType property.
  • ServiceNotRegisteredExceptionResolve failed; exposes .ServiceType and a message that lists the most common causes.

Missing services and optional lookups

The Service Locator itself has no no-op replacement — the registry always initializes, on every Play entry and after Editor assembly reloads. If a service is missing, Resolve<T>() throws ServiceNotRegisteredException immediately. This is intentional: you find out at the exact line that needed the service, not silently later.

If you want graceful handling for an optional service, use TryResolve<T> instead:

if (ServiceLocator.TryResolve<IAudioService>(out var audio))
    Debug.Log("Audio service is available");
else
    Debug.LogWarning("Audio service not registered");

Behavior & edge cases

  • Cache what you resolve. Resolve<T>() is a dictionary lookup every time. Calling it in Update() wastes time each frame. Resolve once in Awake or Start and keep the reference in a field.

  • Main thread only. All public methods assert they run on Unity's main thread in Debug builds. Calling from Task.Run, the thread pool, or any background thread fails with "[ServiceLocator] Main thread only." If a background thread needs a service, resolve it on the main thread first and pass the reference in.

  • Interface or abstract-class keys only. Register<ConcreteClass>(impl) throws ArgumentException. Always use an interface or abstract class as T. This keeps every service swappable without its callers knowing.

  • Interface inheritance is not traversed. If ISaveService derived from some IPersistenceService and you only registered ISaveService, then Resolve<IPersistenceService>() would throw. The registry is an exact-type dictionary. Register both keys against the same instance, or resolve the exact key you registered.

  • Replace does not retarget references you already hold. After a Replace<T>(), new Resolve<T>() calls return the new instance, but any code that cached the old instance keeps using it. Swap implementations before the consumers resolve them — typically in an Awake that runs earlier, or in test setup.

  • Resolving too early. Bootstrap registers all framework services before the first scene loads, so framework services are always available to scene code. But if your own code registers extra services later — for example from a MonoBehaviour's Awake — anything that runs before that point cannot resolve them yet. The ServiceNotRegisteredException message walks you through the common ordering causes.

  • Domain Reload and IL2CPP. The registry is wiped and reinitialized on every Play entry, whether or not Domain Reload is enabled in the Editor. If you build with IL2CPP (Unity's ahead-of-time compilation backend), custom data types you pass through service calls need link.xml entries in your own project so the build's code stripping does not remove them; the framework's own types are already protected.

  • Bootstrap — startup order and the full 23-service list
  • Logger — the first service registered, and the one caveat about replacing it mid-session
  • Save/Load — a typical resolve-and-cache consumer, plus its NullSaveService off-switch
  • Getting Started — your first resolve-and-cache script
  • Manual: Troubleshooting — diagnosing ServiceNotRegisteredException