Service Locator

Intent: Provide a global point of access to a service without coupling users to the concrete class that implements it.

Warning

The book Game Programming Patterns advises using this pattern sparingly. Anytime something is accessible to every part of your program, you risk the same coupling problems as global variables. The book’s first recommendation is always: pass the object to the code that needs it (dependency injection). Only reach for a service locator when passing the object everywhere is genuinely impractical — e.g. a system used in dozens of unrelated subsystems.

Acron Mapping

Pattern role

Acron equivalent

Service interface

Python abstract base class or duck-typed protocol

Service provider

Concrete implementation (e.g. OpenALAudioService, NullAudioService)

Service locator

Module-level registry dict, or a Locator class

Built-in locators

base, taskMgr, messenger, loader — use these directly, don’t wrap them

How to Implement

  1. Define an interface (or just document the expected API on a base class).

  2. Create a Locator module with a registry dict and get(name) / register(name, instance) functions.

  3. At startup (ShowBase.__init__ or a Game init method), register the concrete services.

  4. Game systems call Locator.get('audio').play('bgm') — no import of the concrete class.

  5. For testing or platform swap, call Locator.register('audio', MockAudioService()) before the test runs.

Code Sketch (Python)

# services/locator.py
_registry = {}

def register(name: str, service) -> None:
    _registry[name] = service

def get(name: str):
    if name not in _registry:
        raise KeyError(f"Service '{name}' not registered. Did you call register()?")
    return _registry[name]
# services/audio.py
class NullAudio:
    def play(self, name): pass
    def stop(self, name): pass

class AcronAudio:
    def __init__(self):
        self._sounds = {}

    def play(self, name):
        if name not in self._sounds:
            self._sounds[name] = loader.loadSfx(f'sfx/{name}.ogg')
        self._sounds[name].play()

    def stop(self, name):
        if name in self._sounds:
            self._sounds[name].stop()
# main.py — at startup
from services import locator
from services.audio import AcronAudio, NullAudio

if base.sfxManagerIsValid:
    locator.register('audio', AcronAudio())
else:
    locator.register('audio', NullAudio())  # headless / test mode
# anywhere in game code
from services import locator
locator.get('audio').play('explosion')

Pitfalls

  • Don’t wrap base, taskMgr, messenger: these are already service locators. Adding a locator layer on top adds indirection with no benefit.

  • Null service pattern: always register a Null* fallback (no-op implementation) so unregistered services fail gracefully during testing rather than raising KeyError.

  • Registration order: services registered in __init__ may not yet have base.loader available. Register inside ShowBase.__init__ after super().__init__() completes, or use lazy init in the service itself.