Game Programming Patterns / Communicating Patterns

Service Locator

Intent

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

Motivation

Some objects or systems in a game tend to get around, visiting almost every corner of the codebase. It’s hard to find a part of the game that won’t need a memory allocator, logging, the file system, or random numbers at some point. Systems like those can be thought of as services that need to be available to the entire game.

For our example, we’ll consider audio. It doesn’t have quite the reach of something lower-level like a memory allocator, but it still touches a bunch of game systems: A falling rock hits the ground with a crash (physics). A sniper NPC fires his rifle and a shot rings out (AI). The user selects a menu item with a beep of confirmation (user interface).

Each of these places will need to be able to call into the audio system, with something like one of these:

// Use a static class?
AudioSystem::playSound(VERY_LOUD_BANG);

// Or maybe a singleton?
AudioSystem::instance()->playSound(VERY_LOUD_BANG);

Either gets us where we’re trying to go, but we stumbled into some sticky coupling along the way. Every place in the game calling into our audio system directly references the concrete AudioSystem class and the mechanism for accessing it— either as a static class or a Singleton.

These callsites, of course, have to be coupled to something in order to make a sound play, but letting them poke directly at the concrete audio implementation is like giving a hundred strangers directions to your house just so they can drop a letter on your doorstep. Not only is it a little bit too personal, it’s a real pain when you move and you have to tell each person the new directions.

There’s a better solution: a phone book. People that need to get in touch with us can look us up by name and get our current address. When we move, we tell the phone company. They update the book, and everyone gets the new address. In fact, we don’t even need to give out our real address at all. We can list a P.O. box or some other "representation" of ourselves instead. By having callers go through the book to find us, we have a convenient single place where we control how we’re found.

This is the Service Locator pattern in a nutshell: it decouples code that needs a service from both who it is (the concrete implementation type) and where it is (how we get to the instance of it).

The Pattern

A service class defines an abstract interface to a set of operations. A concrete service provider implements this interface. A separate service locator provides access to the service by finding an appropriate provider while hiding from both the provider’s concrete type and the process used to locate it.

When to Use It

If a large number of places in the codebase need access to the same object and there isn’t an obvious way for them to get it, this pattern is often a good solution. It’s usually more flexible than a static class, and more maintainable than a Singleton.

One limitation is that the implementation of the service doesn’t know who is using it or what for. This means it must be able to work correctly in any circumstance. For example, a class that expects to only be used during the simulation portion of the game loop and not during rendering may not work as a service— it wouldn’t be able to ensure that it’s being used at the right time. So, if our class only expects to be used within a certain context, it may be safest to avoid exposing it to the world with this pattern.

Keep in Mind

The Service is Globally Accessible

This pattern shares a problem with the classic Singleton pattern: it’s global. This is convenient for code that needs the service, but opens the door to coupling and maintenance headaches if the wrong code starts using the service.

The Service Actually Has to Be Located

With a Singleton or a static class, there’s no chance for the instance we need to not be available. Calling code can take for granted that it’s there. But since this pattern has to locate the service, we may need to handle cases where that fails. Fortunately, we’ll cover a strategy later to address this and guarantee that we’ll always get some service when you need it.

Sample Code

Getting back to our audio system problem, let’s address it by exposing the system to the rest of the codebase through a service locator.

The Service

We’ll start off with the audio API. This is the interface that our service will be exposing:

class IAudio
{
public:
  virtual void playSound(int soundID) = 0;
  virtual void stopSound(int soundID) = 0;
  virtual void stopAllSounds() = 0;
};

A real audio engine would be much more complex than this, of course, but this shows the basic idea. What’s important is that it’s an abstract interface class with no implementation bound to it.

The Service Provider

By itself, our audio interface isn’t very useful. We need a concrete implementation. This book isn’t about how to write audio code for a game console, so you’ll have to imagine there’s some actual code in the bodies of these functions, but you get the idea:

class ConsoleAudio : public IAudio
{
public:
  virtual void playSound(int soundID)
  {
    // Play sound using console audio api...
  }

  virtual void stopSound(int soundID)
  {
    // Stop sound using console audio api...
  }

  virtual void stopAllSounds()
  {
    // Stop all sounds using console audio api...
  }
};

Now we have an interface and an implementation. The remaining piece is the service locator— the class that ties the two together.

A Simple Locator

The implementation we’ll see here is about the simplest kind of service locator you’ll see:

class Locator
{
public:
  static IAudio* getAudio() { return service_; }

  static void provide(IAudio* service)
  {
    service_ = service;
  }

private:
  static IAudio* service_;
};

The static getAudio() function does the locating— we can call it from anywhere in the codebase and it will give us back an instance of our IAudio service to use:

void someGameCode()
{
  IAudio *audio = Locator::getAudio();
  audio->playSound(VERY_LOUD_BANG);
}

The way it "locates" is very simple: it relies on some outside code somewhere to register a service provider with it before any other code tries to use the service. When the game is starting up, it will call some code like this:

void initGame()
{
  ConsoleAudio *audio = new ConsoleAudio();
  Locator::provide(audio);
}

The key part to notice here is that our someGameCode() function isn’t aware of the concrete ConsoleAudio class, just the abstract IAudio interface. Equally important, not even the locator class is coupled to the concrete service provider. The only place in code that knows about the actual concrete class is the initialization function that registers the service.

There’s one more level of decoupling here: the IAudio interface isn’t aware of the fact that it’s being accessed in most places through a service locator. As far as it knows, it’s just a regular abstract base class. This is useful because it means we can apply this pattern to existing classes that weren’t necessarily designed around it. This is in contrast with Singleton, which affects the design of the "service" class itself.

A Null Service

Our implementation so far is certainly simple, and it’s pretty flexible too. But it has one big shortcoming: if we try to use the service before a provider has been registered, it returns NULL. If the calling code doesn’t check that, we’re going to crash the game.

Fortunately, there’s another design pattern called "Null Object" we can use to address this. The basic idea is that in places where we would return NULL when we fail to find or create an object, we instead return a special object that implements the same interface as the desired object. Its implementation basically does nothing, but allows code that receives the object to safely continue on as if it had received a "real" one.

To use this, we’ll define another "null" service provider:

class NullAudio: public IAudio
{
public:
  virtual void playSound(int soundID) { /* Do nothing. */ }
  virtual void stopSound(int soundID) { /* Do nothing. */ }
  virtual void stopAllSounds()        { /* Do nothing. */ }
};

As you can see, it implements the service interface, but doesn’t actually do anything. Now, we change our locator to this:

class Locator
{
public:
  static void initialize() { service_ = &nullService_; }

  static IAudio& getAudio() { return *service_; }

  static void provide(IAudio* service)
  {
    if (service == NULL)
    {
      // Revert to null service.
      service_ = &nullService_;
    }
    else
    {
      service_ = service;
    }
  }

private:
  static IAudio* service_;
  static NullAudio nullService_;
};

Calling code will never know that a "real" service wasn’t found, nor does it have to worry about handling NULL. It’s guaranteed to always get back a valid object.

This is also useful for intentionally failing to find services. If we want to disable a system temporarily, we now have an easy way to do so: simply don’t register a provider for the service and the locator will default to a null provider.

Logging Decorator

Now that our system is pretty robust, let’s discuss another refinement this pattern lets us do: decorated services. I’ll explain with an example:

It’s helpful during development to have the game log when certain events occur. If you’re working on AI, it’s very useful to know when an entity is changing AI states. If you’re the sound programmer, you may want a record of every sound as it plays so you can check that they trigger in the right order.

The typical solution is to just litter the code with calls to some log() function. Unfortunately, that replaces one problem with another: now we have too much logging. The AI coder really doesn’t care when sounds are playing, and the sound person doesn’t care about AI state transitions, but now they both have to wade through each other’s messages.

Ideally, we would be able to selectively enable logging for just the stuff we care about, and in the final game build, there’d be no logging at all. If the different systems we want to conditionally log are exposed as services, then we can solve this using the Decorator pattern. Let’s define another audio service provider implementation like this:

class LoggedAudio : public IAudio
{
public:
  LoggedAudio(IAudio &wrapped) : wrapped_(wrapped) {}

  virtual void playSound(int soundID)
  {
    log("play sound");
    wrapped_.playSound(soundID);
  }

  virtual void stopSound(int soundID)
  {
    log("stop sound");
    wrapped_.stopSound(soundID);
  }

  virtual void stopAllSounds()
  {
    log("stop all sounds");
    wrapped_.stopAllSounds();
  }

private:
  void log(const char* message) { /* Code to log message... */ }

  IAudio &wrapped_;
};

As you can see, it wraps another audio provider and exposes the same interface. It forwards the actual audio behavior to the inner provider, but also logs each sound call. If a programmer wants to enable audio logging, they call this:

void enableAudioLogging()
{
  // Decorate the existing service.
  IAudio *service = new LoggedAudio(Locator::getAudio());

  // Swap it in.
  Locator::provide(service);
}

Now any calls to the audio service will be logged before continuing as before. And, of course, this plays nicely with our null service, so you can both disable audio and yet still log the sounds that it would play if sound were enabled.

Design Decisions

We’ve covered a typical implementation, but there’s a couple of ways that it can vary, based on differing answers to a few core questions:

How Is the Service Located?

What happens if service could not be located?

Among these options, the one I see used most frequently is simply asserting that the service will be found. By the time a game gets out the door, it’s been very heavily tested, and it will likely be run on a reliable piece of hardware. The chances of a service failing to be found by then are pretty slim.

On a larger team, I encourage you to throw a null service in. It doesn’t take much effort to implement, and can spare you from some downtime during development when a service isn’t available. It also gives you an easy way to turn a service off if it’s buggy or is just distracting you from what you’re working on.

What Is the Scope of the Service?

Up to this point, we’ve assumed that the locator will provide access to the service to anyone who wants it. While this is the typical way the pattern is used, another option is to limit access to a single class and its descendants, like so:

class Base
{
  // Code to locate service and set service_...

protected:
  // Derived classes can use service
  static IAudio& getAudio() { return *service_; }

private:
  static IAudio* service_;
};

With this, access to the service is restricted to classes that inherit Base. There are advantages either way:

My general guideline is that if the service is restricted to a single domain in the game then limit its scope to a class. For example, a service for getting access to the network can probably be limited to online classes. Services that get used more widely like logging should be global.

See Also