Service Locator - UQcsse3200/2024-studio-2 GitHub Wiki

This game uses the Service Locator pattern to provide global access to some key services. ServiceLocator (code) itself is a static class which can be accessed directly. For example:

GameTime gameTime = ServiceLocator.getTimeSource();
PhysicsService physics = ServiceLocator.getPhysicsService();

Accessible services are:

  • Dialogue Box Service: is responsible for managing the display of textual hints or messages on the screen in the form of chat overlays. A chat overlay is a user interface element that presents messages to the player, such as hints or quest-related information.
  • Entity Service: Keeps track of all entities that exist in the game, updating them each frame. This has to be global so that each new entity can register itself.
  • Physics Service: Keeps track of physics-enabled entities, updating them according to the physics simulation each frame.
  • Render Service: Keeps track of rendered entities, drawing them each frame.
  • Resource Service: Provides a way to load and access assets from anywhere, while ensuring that the same asset is not loaded more than once.
  • Input Service: Provides access to user input.
  • Time Source: The recommended way to get the current time or calculate how much time has passed. The time source can be mocked for unit tests to prevent tests being time-dependant.
  • In-Game Time Service (InGameTime): Manages the in-game clock that can be paused and resumed independently of the real-world time. This service is crucial for in-game events and mechanics that need to be paused (e.g., when opening a menu).
  • Day-Night Cycle Service: Controls the ambient lighting and simulates the passage of day and night within the game world. It adjusts the lighting based on the in-game time to create a realistic day-night cycle.

Adding new Services

Read this before you make changes to the service locator!

Most access problems ("how do I get access to this instance?") can be easily solved with global access, but this is rarely the correct solution, and has been used sparingly in the game engine. Only add a new service if you answer no to all of these statements:

  • My service is connected to a single game screen or game area. If so, consider initialising your instance inside that class instead. For example, the player entity in the provided code is connected only to the ForestGameArea class. It wouldn't make sense to have this be accessible globally, since we don't need a player entity on the main menu screen, settings screen, etc.
  • The classes that need my service are instantiated in a single place. If all the classes that need this service are created in one place, such as a game area or factory, consider attaching the service to that area or factory instead.
  • I may need more than one of these sometime in the future. Providing global access to the service means locking yourself into always having that single instance of the service. Think carefully about future game features that may break this assumption. For example: What if we added local multiplayer? What if we added popup menus? What if we added more game areas?

Behind the Scenes

Why not just use singletons?

Singletons are generally advised against and considered as anti-patterns, due to their drawbacks:

  • Providing global access to classes around the code base decreases readability and often leads to bugs. Any function can have the side effect of modifying external state. Service locator still has this problem, but limits which classes are globally accessible and restricts access through one place.
  • Singletons encourage excessive coupling to global components.
  • We do not necessarily want to restrict ourselves to a single instance of a class. For example, we might be tempted to use a singleton for the game's input. If we then want to add support for local multi-player, we have to refactor code all over the code base where the singleton was used.
  • When 'lazy-loaded' (i.e. instantiated on first access rather than at the start of application), we give up some control of the lifecycle of that class. Service locator instead lets us choose exactly when we want to initialize our services.

Why not use dependency injection?

Dependency injection (DI) is a great way to keep code decoupled without relying on global instances, and avoids many of the problems listed above. However, DI can be difficult to learn and understand, since inversion of control goes against the intuitive ideas of abstraction and encapsulation. You are highly encouraged to use this pattern in the future, but for simplicity it is not heavily used in the game (other than when exposing dependencies for unit testing).

Further Reading