Game Programming - CSCI 3213
Spring 2026 - Lecture 15 (Final)
Oklahoma City University
As we implement this pattern, we'll be creating multiple files in a specific order. You will see errors in Unity and your IDE until all files are complete.
As Apollo 13's Fred Haise said: "Never Panic Early"
This is a critical skill for software development. Note problems, but stay focused on the implementation path.
Think about dining at a restaurant:
Maintain a central registry of services and provide a mechanism to locate and access them globally.
You need global access to services (logging, analytics, ads) but don't want tight coupling or complex initialization throughout your codebase.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client Code โ
โ (Requests services) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ GetService()
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Service Locator โ โ Central Registry
โ (Dictionary of services) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ Returns instance
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Service Providers โ
โ - Logger (ILoggerService) โ
โ - Analytics (IAnalyticsService) โ
โ - Ads (IAdvertisement) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Service | Use Service Locator? | Reason |
|---|---|---|
| Logger | โ Yes | Needed everywhere, non-gameplay |
| Analytics | โ Yes | Global tracking, removable |
| HUD | โ No | Scene-specific, not always loaded |
| PlayerController | โ No | Core gameplay, explicit dependency |
Create a new file: ServiceLocator.cs
Static class that manages service registration and retrieval.
using System;
using System.Collections.Generic;
public static class ServiceLocator
{
// Central registry: Type โ Instance
private static readonly IDictionary<Type, object> Services =
new Dictionary<Type, object>();
// Register a service
public static void RegisterService<T>(T service)
{
if (!Services.ContainsKey(typeof(T)))
{
Services[typeof(T)] = service;
}
else
{
throw new ApplicationException(
"Service already registered");
}
}
// Retrieve a service
public static T GetService<T>()
{
try
{
return (T)Services[typeof(T)];
}
catch
{
throw new ApplicationException(
"Requested service not found.");
}
}
}
Create separate interface files for best practices:
Assets/Scripts/Services/Interfaces/ILoggerService.cs
Assets/Scripts/Services/Interfaces/IAnalyticsService.cs
Assets/Scripts/Services/Interfaces/IAdvertisement.cs
โ ๏ธ These interface files go into your Unity project for Blade Racer.
// Logger service contract
public interface ILoggerService
{
void Log(string message);
}
// Analytics service contract
public interface IAnalyticsService
{
void SendEvent(string eventName);
}
// Advertisement service contract
public interface IAdvertisement
{
void DisplayAd();
}
Create a new file: Assets/Scripts/Services/Logger.cs
This is the concrete implementation of the ILoggerService interface.
โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
public class Logger : ILoggerService
{
public void Log(string message)
{
Debug.Log("Logged: " + message);
}
}
// Enhanced logger example
public void Log(string message, LogLevel level = LogLevel.Info)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string logEntry = $"[{timestamp}] [{level}] {message}";
Debug.Log(logEntry);
WriteToFile(logEntry);
if (level == LogLevel.Error)
SendToAnalytics(logEntry);
}
Create a new file: Assets/Scripts/Services/Analytics.cs
This is the concrete implementation of the IAnalyticsService interface.
โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
public class Analytics : IAnalyticsService
{
public void SendEvent(string eventName)
{
Debug.Log("Sent: " + eventName);
}
}
public class UnityAnalytics : IAnalyticsService
{
public void SendEvent(string eventName)
{
// Unity Analytics
Unity.Analytics.Analytics.CustomEvent(eventName);
}
}
public class GoogleAnalytics : IAnalyticsService
{
public void SendEvent(string eventName)
{
// Google Analytics for Games
FirebaseAnalytics.LogEvent(eventName);
}
}
Create a new file: Assets/Scripts/Services/Advertisement.cs
This is the concrete implementation of the IAdvertisement interface.
โ ๏ธ This code goes into your Unity project for Blade Racer.
using UnityEngine;
public class Advertisement : IAdvertisement
{
public void DisplayAd()
{
Debug.Log("Displaying video advertisement");
}
}
using UnityEngine.Advertisements;
public class UnityAds : IAdvertisement
{
private string gameId = "1234567";
private string placementId = "rewardedVideo";
public void DisplayAd()
{
if (Advertisement.IsReady(placementId))
{
Advertisement.Show(placementId);
}
else
{
Debug.LogWarning("Ad not ready");
}
}
Create a new file: Assets/Scripts/Testing/ClientServiceLocator.cs
This script tests your Service Locator implementation by registering and using services.
โ ๏ธ This is temporary testing code - you can remove it after testing your implementation.
using UnityEngine;
public class ClientServiceLocator : MonoBehaviour
{
void Start()
{
RegisterServices();
}
private void RegisterServices()
{
// Register Logger
ILoggerService logger = new Logger();
ServiceLocator.RegisterService(logger);
// Register Analytics
IAnalyticsService analytics = new Analytics();
ServiceLocator.RegisterService(analytics);
// Register Advertisement
IAdvertisement advertisement = new Advertisement();
ServiceLocator.RegisterService(advertisement);
}
void OnGUI()
{
GUILayout.Label("Review output in the console:");
if (GUILayout.Button("Log Event"))
{
ILoggerService logger =
ServiceLocator.GetService<ILoggerService>();
logger.Log("Hello World!");
}
if (GUILayout.Button("Send Analytics"))
{
IAnalyticsService analytics =
ServiceLocator.GetService<IAnalyticsService>();
analytics.SendEvent("button_clicked");
}
if (GUILayout.Button("Display Advertisement"))
{
IAdvertisement advertisement =
ServiceLocator.GetService<IAdvertisement>();
advertisement.DisplayAd();
}
}
}
TestPanel.cs to combine all test controls into one draggable window.
See the Event Bus lecture for the unified TestPanel implementation.
Add Service Locator keyboard shortcuts and section to your TestPanel.cs.
DrawKeymapWindow():L = Log Event, Y = Send Analytics, H = Display Ad (sHow)
ClientServiceLocator scriptprivate void RegisterServices()
{
#if UNITY_EDITOR || DEBUG
// Debug builds: verbose logging
ILoggerService logger = new VerboseLogger();
#else
// Release builds: minimal logging
ILoggerService logger = new MinimalLogger();
#endif
ServiceLocator.RegisterService(logger);
#if UNITY_EDITOR
// Don't send analytics during development
IAnalyticsService analytics = new MockAnalytics();
#else
// Production: real analytics
IAnalyticsService analytics = new UnityAnalytics();
#endif
ServiceLocator.RegisterService(analytics);
}
private void RegisterServices()
{
IAdvertisement ads;
#if UNITY_IOS
ads = new iOSAds();
#elif UNITY_ANDROID
ads = new AndroidAds();
#elif UNITY_WEBGL
ads = new WebGLAds();
#else
ads = new MockAds(); // Editor/unknown platform
#endif
ServiceLocator.RegisterService(ads);
// Platform-specific analytics
#if UNITY_IOS || UNITY_ANDROID
IAnalyticsService analytics = new MobileAnalytics();
#else
IAnalyticsService analytics = new DesktopAnalytics();
#endif
ServiceLocator.RegisterService(analytics);
}
public static void UnregisterService<T>()
{
if (Services.ContainsKey(typeof(T)))
{
Services.Remove(typeof(T));
}
}
public static void UnregisterAllServices()
{
Services.Clear();
}
public static T TryGetService<T>() where T : class
{
if (Services.TryGetValue(typeof(T), out object service))
{
return service as T;
}
return null;
}
// Usage
var logger = ServiceLocator.TryGetService<ILoggerService>();
if (logger != null)
{
logger.Log("Service found!");
}
public static bool HasService<T>()
{
return Services.ContainsKey(typeof(T));
}
// Usage in client code
public class PlayerController : MonoBehaviour
{
void Start()
{
// Check before using
if (ServiceLocator.HasService<IAnalyticsService>())
{
var analytics = ServiceLocator.GetService<IAnalyticsService>();
analytics.SendEvent("player_spawned");
}
}
void OnDestroy()
{
// Safe even if analytics not registered
var analytics = ServiceLocator.TryGetService<IAnalyticsService>();
analytics?.SendEvent("player_destroyed");
}
}
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
// Singleton pattern
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// Register all services early
RegisterServices();
}
private void RegisterServices()
{
ServiceLocator.RegisterService<ILoggerService>(
new Logger());
ServiceLocator.RegisterService<IAnalyticsService>(
new UnityAnalytics());
ServiceLocator.RegisterService<IAdvertisement>(
new UnityAds());
Debug.Log("All services registered");
}
}
// Mock logger that doesn't actually log
public class MockLogger : ILoggerService
{
public List<string> LoggedMessages = new List<string>();
public void Log(string message)
{
LoggedMessages.Add(message);
// No Debug.Log - silent!
}
}
// Unit test
[Test]
public void PlayerController_OnSpawn_LogsEvent()
{
// Arrange
var mockLogger = new MockLogger();
ServiceLocator.RegisterService<ILoggerService>(mockLogger);
var player = new GameObject().AddComponent<PlayerController>();
// Act
player.Spawn();
// Assert
Assert.AreEqual(1, mockLogger.LoggedMessages.Count);
Assert.AreEqual("Player spawned", mockLogger.LoggedMessages[0]);
// Cleanup
ServiceLocator.UnregisterService<ILoggerService>();
}
public class PlayerController : MonoBehaviour
{
private ILoggerService _logger;
private IAnalyticsService _analytics;
void Start()
{
_logger = ServiceLocator.GetService<ILoggerService>();
_analytics = ServiceLocator.GetService<IAnalyticsService>();
_logger.Log("Player controller initialized");
_analytics.SendEvent("player_session_start");
}
public void CollectCoin()
{
_logger.Log("Coin collected");
_analytics.SendEvent("coin_collected");
// ... game logic
}
public void Die()
{
_logger.Log("Player died");
_analytics.SendEvent("player_death");
// Show rewarded ad?
var ads = ServiceLocator.TryGetService<IAdvertisement>();
ads?.DisplayAd();
}
}
A technique where objects receive their dependencies from external sources rather than creating them.
// Dependencies are explicit!
public class PlayerController
{
private readonly ILoggerService _logger;
private readonly IAnalyticsService _analytics;
// Constructor injection - dependencies visible
public PlayerController(
ILoggerService logger,
IAnalyticsService analytics)
{
_logger = logger;
_analytics = analytics;
}
public void Die()
{
_logger.Log("Player died");
_analytics.SendEvent("player_death");
}
}
| Aspect | Service Locator | Dependency Injection |
|---|---|---|
| Dependencies | Hidden in method calls | Explicit in constructor |
| Learning Curve | Easy to learn | Steeper (especially with frameworks) |
| Compile-Time Safety | Runtime errors | Compile-time errors |
| Testing | Need to set up locator | Inject mocks directly |
| Flexibility | Very flexible | More structured |
// Installer - registers services
public class GameInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<ILoggerService>()
.To<Logger>()
.AsSingle();
Container.Bind<IAnalyticsService>()
.To<UnityAnalytics>()
.AsSingle();
}
}
// Usage - constructor injection
public class PlayerController : MonoBehaviour
{
private ILoggerService _logger;
private IAnalyticsService _analytics;
// Zenject injects dependencies automatically
[Inject]
public void Construct(
ILoggerService logger,
IAnalyticsService analytics)
{
_logger = logger;
_analytics = analytics;
}
}
// BAD - crashes if not registered
var logger = ServiceLocator.GetService<ILoggerService>();
// GOOD - handles gracefully
var logger = ServiceLocator.TryGetService<ILoggerService>();
logger?.Log("Message");
// BAD - tight coupling
ServiceLocator.RegisterService(new Logger());
var logger = ServiceLocator.GetService<Logger>();
// GOOD - loose coupling
ServiceLocator.RegisterService<ILoggerService>(new Logger());
var logger = ServiceLocator.GetService<ILoggerService>();
// BAD - lookup every frame
void Update()
{
var logger = ServiceLocator.GetService<ILoggerService>();
logger.Log("Frame update");
}
// GOOD - cache in Start()
private ILoggerService _logger;
void Start()
{
_logger = ServiceLocator.GetService<ILoggerService>();
}
void Update()
{
_logger.Log("Frame update");
}
private IAnalyticsService _analytics;
public IAnalyticsService Analytics
{
get
{
if (_analytics == null)
_analytics = ServiceLocator.GetService<IAnalyticsService>();
return _analytics;
}
}
public static void RegisterService<T>(T service)
{
string typeName = typeof(T).Name;
if (!Services.ContainsKey(typeof(T)))
{
Services[typeof(T)] = service;
Debug.Log($"[ServiceLocator] Registered: {typeName}");
}
else
{
Debug.LogError($"[ServiceLocator] Already registered: {typeName}");
throw new ApplicationException("Service already registered");
}
}
public static T GetService<T>()
{
string typeName = typeof(T).Name;
if (Services.TryGetValue(typeof(T), out object service))
{
Debug.Log($"[ServiceLocator] Retrieved: {typeName}");
return (T)service;
}
Debug.LogError($"[ServiceLocator] Service not found: {typeName}");
throw new ApplicationException("Requested service not found.");
}
public static void LogAllServices()
{
Debug.Log("=== Registered Services ===");
foreach (var kvp in Services)
{
Debug.Log($"{kvp.Key.Name}: {kvp.Value.GetType().Name}");
}
}
| Problem | Pattern |
|---|---|
| Need exactly one instance | Singleton |
| Creating/destroying is expensive | Object Pool |
| Incompatible interfaces | Adapter |
| Complex subsystem needs simplification | Facade |
| Add features at runtime | Decorator |
| Notify multiple objects of changes | Observer |
| Swap algorithms at runtime | Strategy |
| Operations on object structures | Visitor |
| Global service access | Service Locator |
| Spatial queries (collision, neighbors) | Spatial Partition |
In 2017, PlayerUnknown's Battlegrounds (PUBG) popularized the Battle Royale genre, followed by Epic's Fortnite in 2018. These games pioneered the "live service" model - games that continuously evolve with seasons, events, and updates. Behind the scenes, they require an ecosystem of constantly-running services: matchmaking, anti-cheat, analytics, progression tracking, item shops, and social features.
The challenge? Any component in the game might need to access these services: UI needs the shop, gameplay needs analytics, inventory needs progression tracking. Hard-coding dependencies would create a tangled mess. Instead, modern live service games use service locators - centralized registries where any system can find the service it needs without tight coupling. When Epic adds a new seasonal feature, they register it as a service, and existing code can discover it dynamically.
Live service games are Service Locator implementations at massive scale! Just like our ServiceLocator provides global access to ILogService, IAdsService, and IAnalyticsService, Fortnite's architecture provides global access to MatchmakingService, ProgressionService, ShopService, etc. Any gameplay code can call ServiceLocator.Get<IMatchmaking>() without knowing how matchmaking works. This loose coupling enables rapid feature iteration - Epic ships updates every week because services are decoupled and discoverable!
You've completed Game Programming CSCI 3213!
Thank you for an amazing semester!
Questions? Office Hours: Monday/Wednesday 2:00-4:00 PM
SSM 204A - bobby.reed@okcu.edu
๐ฎ Good luck with your game development journey! ๐