Game Programming - CSCI 3213
Spring 2026 - Lecture 15 (Final)
Oklahoma City University
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 |
using System;
using System.Collections.Generic;
namespace Chapter.ServiceLocator
{
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.");
}
}
}
}
namespace Chapter.ServiceLocator
{
// 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();
}
}
using UnityEngine;
namespace Chapter.ServiceLocator
{
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);
}
using UnityEngine;
namespace Chapter.ServiceLocator
{
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);
}
}
using UnityEngine;
namespace Chapter.ServiceLocator
{
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");
}
}
}
using UnityEngine;
namespace Chapter.ServiceLocator
{
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();
}
}
}
}
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 |
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! ๐