Services
The Popcorning Guinea Pig engine employs a service-oriented architecture that promotes loose coupling, testability, and maintainability. Services provide reusable functionality across your application through well-defined interfaces and dependency injection patterns.
Overview
The service system is built around several key principles:
- Interface-Based Design: All services implement interfaces for flexibility and testability
- Dependency Injection: Services are injected into classes that need them
- Singleton Management: Services are typically managed as singletons by the engine
- Lifecycle Management: Services have clear initialization and cleanup patterns
Core Service Architecture
Service Interface Pattern
All services follow a consistent interface-based pattern:
// Define the service interface
public interface ISoundPlayer
{
Task PlaySound(string soundPath, float volume = 1.0f);
Task PlayMusic(string musicPath, bool loop = true);
void StopMusic();
void SetMusicVolume(float volume);
void SetSoundVolume(float volume);
void Dispose();
}
// Implement the service
public class SoundPlayer : ISoundPlayer
{
// Implementation details...
public async Task PlaySound(string soundPath, float volume = 1.0f)
{
// Platform-specific sound playing logic
var sound = Raylib.LoadSound(soundPath);
Raylib.SetSoundVolume(sound, volume);
Raylib.PlaySound(sound);
}
public void Dispose()
{
// Cleanup resources
StopMusic();
// Unload cached sounds
}
}
Service Registration and Access
Services are registered with the engine and accessed through the Game instance:
public class MyGame : Game
{
private ISoundPlayer soundPlayer;
private ISettingsService settingsService;
public override void Load()
{
// Access services through dependency injection
soundPlayer = GetService<ISoundPlayer>();
settingsService = GetService<ISettingsService>();
// Use services
await soundPlayer.PlayMusic("assets/music/menu-theme.ogg");
var volume = settingsService.GetSetting<float>("MusicVolume", 0.7f);
soundPlayer.SetMusicVolume(volume);
}
}
Built-in Services
The engine provides several built-in services that cover common game development needs:
Audio Services
ISoundPlayer - Audio playback and management
- Sound effects and music playback
- Volume control and audio management
- Platform-specific audio optimization
- Resource management for audio assets
// Example usage
var soundPlayer = Game.Instance.GetService<ISoundPlayer>();
await soundPlayer.PlaySound("assets/sounds/button-click.wav");
soundPlayer.SetSoundVolume(0.8f);
Settings Management
ISettingsService - Configuration and user preferences
- Persistent settings storage
- Type-safe setting access
- Default value handling
- Settings change notifications
// Example usage
var settings = Game.Instance.GetService<ISettingsService>();
settings.SetSetting("GraphicsQuality", "High");
var quality = settings.GetSetting<string>("GraphicsQuality", "Medium");
Localization Services
ILocalizationService - Multi-language support
- String localization and translation
- Language switching and management
- Pluralization and formatting support
- Resource-based translation loading
// Example usage
var localization = Game.Instance.GetService<ILocalizationService>();
string welcomeText = localization.GetString("welcome_message");
string scoreText = localization.GetString("score_format", playerScore);
Input Management
IInputService - Input handling and management
- Keyboard, mouse, and gamepad support
- Input context management
- Action mapping and binding
- Input state tracking
// Example usage
var input = Game.Instance.GetService<IInputService>();
var gameContext = new InputContext("gameplay", priority: 100);
gameContext.AddBinding(KeyboardKey.KEY_SPACE, "jump");
input.PushContext("gameplay", gameContext);
Asset Management
IAssetRegistry - Resource loading and caching
- Asset registration and lookup
- Memory management for resources
- Asset type handling
- Shared resource optimization
// Example usage
var assets = Game.Instance.GetService<IAssetRegistry>();
assets.RegisterAsset("player-texture", "assets/player.png", AssetType.Texture);
var playerTexture = assets.GetAsset<Texture2D>("player-texture");
Platform Integration
IDiscordService - Discord Rich Presence integration
- Game status display in Discord
- Rich presence information
- Activity updates and management
- Social integration features
// Example usage
var discord = Game.Instance.GetService<IDiscordService>();
await discord.SetActivity("Playing Campaign", "Level 5", "large_image");
ISteamService - Steam platform integration
- Steam API integration
- Achievement management
- Cloud saves and user data
- Steam overlay support
// Example usage
var steam = Game.Instance.GetService<ISteamService>();
steam.UnlockAchievement("FIRST_VICTORY");
bool hasAchievement = steam.HasAchievement("SPEED_RUN");
Service Implementation Patterns
Basic Service Implementation
public interface IGameStateService
{
int CurrentScore { get; set; }
int CurrentLevel { get; set; }
PlayerProfile CurrentPlayer { get; set; }
void SaveGameState();
Task<bool> LoadGameStateAsync();
void ResetGameState();
event Action<int> OnScoreChanged;
event Action<int> OnLevelChanged;
}
public class GameStateService : IGameStateService
{
private int _currentScore;
private int _currentLevel;
private PlayerProfile _currentPlayer;
public int CurrentScore
{
get => _currentScore;
set
{
if (_currentScore != value)
{
_currentScore = value;
OnScoreChanged?.Invoke(value);
}
}
}
public int CurrentLevel
{
get => _currentLevel;
set
{
if (_currentLevel != value)
{
_currentLevel = value;
OnLevelChanged?.Invoke(value);
}
}
}
public PlayerProfile CurrentPlayer { get; set; }
public event Action<int> OnScoreChanged;
public event Action<int> OnLevelChanged;
public void SaveGameState()
{
var gameState = new GameStateData
{
Score = CurrentScore,
Level = CurrentLevel,
Player = CurrentPlayer,
Timestamp = DateTime.UtcNow
};
// Save to persistent storage
var json = JsonSerializer.Serialize(gameState);
File.WriteAllText("gamestate.json", json);
}
public async Task<bool> LoadGameStateAsync()
{
try
{
if (!File.Exists("gamestate.json"))
return false;
var json = await File.ReadAllTextAsync("gamestate.json");
var gameState = JsonSerializer.Deserialize<GameStateData>(json);
CurrentScore = gameState.Score;
CurrentLevel = gameState.Level;
CurrentPlayer = gameState.Player;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load game state: {ex.Message}");
return false;
}
}
public void ResetGameState()
{
CurrentScore = 0;
CurrentLevel = 1;
CurrentPlayer = new PlayerProfile { Name = "Player" };
}
}
Service with Dependencies
public interface IPlayerService
{
PlayerProfile GetCurrentPlayer();
Task<bool> SavePlayerAsync(PlayerProfile player);
Task<PlayerProfile> LoadPlayerAsync(string playerId);
void UpdatePlayerStats(string statName, object value);
}
public class PlayerService : IPlayerService
{
private readonly ISettingsService _settings;
private readonly ILocalizationService _localization;
private PlayerProfile _currentPlayer;
public PlayerService(ISettingsService settings, ILocalizationService localization)
{
_settings = settings;
_localization = localization;
}
public PlayerProfile GetCurrentPlayer()
{
if (_currentPlayer == null)
{
// Load default player or create new
var defaultName = _localization.GetString("default_player_name");
_currentPlayer = new PlayerProfile { Name = defaultName };
}
return _currentPlayer;
}
public async Task<bool> SavePlayerAsync(PlayerProfile player)
{
try
{
// Use settings service for persistence
_settings.SetSetting("PlayerData", JsonSerializer.Serialize(player));
_currentPlayer = player;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save player: {ex.Message}");
return false;
}
}
public async Task<PlayerProfile> LoadPlayerAsync(string playerId)
{
try
{
var playerData = _settings.GetSetting<string>($"Player_{playerId}", null);
if (playerData != null)
{
return JsonSerializer.Deserialize<PlayerProfile>(playerData);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load player {playerId}: {ex.Message}");
}
// Return default player if loading fails
return new PlayerProfile { Name = _localization.GetString("default_player_name") };
}
public void UpdatePlayerStats(string statName, object value)
{
if (_currentPlayer != null)
{
_currentPlayer.Stats[statName] = value;
// Auto-save player data
_ = SavePlayerAsync(_currentPlayer);
}
}
}
Async Service Implementation
public interface INetworkService
{
Task<bool> ConnectAsync(string serverUrl);
Task DisconnectAsync();
Task<T> SendRequestAsync<T>(string endpoint, object data);
bool IsConnected { get; }
event Action OnConnected;
event Action<string> OnDisconnected;
event Action<NetworkMessage> OnMessageReceived;
}
public class NetworkService : INetworkService
{
private HttpClient _httpClient;
private string _baseUrl;
private bool _isConnected;
public bool IsConnected => _isConnected;
public event Action OnConnected;
public event Action<string> OnDisconnected;
public event Action<NetworkMessage> OnMessageReceived;
public async Task<bool> ConnectAsync(string serverUrl)
{
try
{
_baseUrl = serverUrl;
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
// Test connection
var response = await _httpClient.GetAsync("/health");
if (response.IsSuccessStatusCode)
{
_isConnected = true;
OnConnected?.Invoke();
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"Network connection failed: {ex.Message}");
}
return false;
}
public async Task DisconnectAsync()
{
if (_httpClient != null)
{
_httpClient.Dispose();
_httpClient = null;
}
if (_isConnected)
{
_isConnected = false;
OnDisconnected?.Invoke("Manual disconnect");
}
}
public async Task<T> SendRequestAsync<T>(string endpoint, object data)
{
if (!_isConnected)
throw new InvalidOperationException("Not connected to server");
try
{
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(endpoint, content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(responseJson);
}
catch (Exception ex)
{
Console.WriteLine($"Network request failed: {ex.Message}");
throw;
}
}
public void Dispose()
{
_ = DisconnectAsync();
}
}
Service Registration
Manual Service Registration
public class MyGame : Game
{
public override void Load()
{
// Register services manually
var soundPlayer = new SoundPlayer();
RegisterService<ISoundPlayer>(soundPlayer);
var settingsService = new SettingsService();
RegisterService<ISettingsService>(settingsService);
// Register service with dependencies
var playerService = new PlayerService(
GetService<ISettingsService>(),
GetService<ILocalizationService>()
);
RegisterService<IPlayerService>(playerService);
}
}
Factory-Based Registration
public class ServiceFactory
{
public static T CreateService<T>() where T : class
{
if (typeof(T) == typeof(ISoundPlayer))
{
return new SoundPlayer() as T;
}
else if (typeof(T) == typeof(ISettingsService))
{
return new SettingsService() as T;
}
else if (typeof(T) == typeof(INetworkService))
{
var networkService = new NetworkService();
// Configure network service
return networkService as T;
}
throw new NotSupportedException($"Service type {typeof(T)} not supported");
}
}
// Usage in Game class
public override void Load()
{
RegisterService<ISoundPlayer>(ServiceFactory.CreateService<ISoundPlayer>());
RegisterService<ISettingsService>(ServiceFactory.CreateService<ISettingsService>());
}
Service Communication Patterns
Event-Based Communication
public class GameCoordinator
{
private readonly IGameStateService _gameState;
private readonly ISoundPlayer _soundPlayer;
private readonly IPlayerService _playerService;
public GameCoordinator()
{
_gameState = Game.Instance.GetService<IGameStateService>();
_soundPlayer = Game.Instance.GetService<ISoundPlayer>();
_playerService = Game.Instance.GetService<IPlayerService>();
// Subscribe to service events
_gameState.OnScoreChanged += HandleScoreChanged;
_gameState.OnLevelChanged += HandleLevelChanged;
}
private async void HandleScoreChanged(int newScore)
{
// Play score change sound
await _soundPlayer.PlaySound("assets/sounds/score-increase.wav");
// Update player stats
_playerService.UpdatePlayerStats("HighScore", newScore);
// Check for score milestones
if (newScore % 1000 == 0)
{
await _soundPlayer.PlaySound("assets/sounds/milestone.wav");
}
}
private async void HandleLevelChanged(int newLevel)
{
// Play level up sound
await _soundPlayer.PlaySound("assets/sounds/level-up.wav");
// Update player level
_playerService.UpdatePlayerStats("CurrentLevel", newLevel);
}
}
Service Mediator Pattern
public interface IServiceMediator
{
void PublishEvent<T>(T eventData) where T : class;
void Subscribe<T>(Action<T> handler) where T : class;
void Unsubscribe<T>(Action<T> handler) where T : class;
}
public class ServiceMediator : IServiceMediator
{
private readonly Dictionary<Type, List<Delegate>> _handlers = new();
public void PublishEvent<T>(T eventData) where T : class
{
var eventType = typeof(T);
if (_handlers.ContainsKey(eventType))
{
foreach (var handler in _handlers[eventType].Cast<Action<T>>())
{
try
{
handler(eventData);
}
catch (Exception ex)
{
Console.WriteLine($"Event handler error: {ex.Message}");
}
}
}
}
public void Subscribe<T>(Action<T> handler) where T : class
{
var eventType = typeof(T);
if (!_handlers.ContainsKey(eventType))
{
_handlers[eventType] = new List<Delegate>();
}
_handlers[eventType].Add(handler);
}
public void Unsubscribe<T>(Action<T> handler) where T : class
{
var eventType = typeof(T);
if (_handlers.ContainsKey(eventType))
{
_handlers[eventType].Remove(handler);
}
}
}
// Event data classes
public class ScoreChangedEvent
{
public int OldScore { get; set; }
public int NewScore { get; set; }
public int Difference => NewScore - OldScore;
}
public class PlayerLevelUpEvent
{
public int NewLevel { get; set; }
public PlayerProfile Player { get; set; }
}
// Usage in services
public class GameStateService : IGameStateService
{
private readonly IServiceMediator _mediator;
public GameStateService(IServiceMediator mediator)
{
_mediator = mediator;
}
public int CurrentScore
{
get => _currentScore;
set
{
var oldScore = _currentScore;
_currentScore = value;
// Publish event through mediator
_mediator.PublishEvent(new ScoreChangedEvent
{
OldScore = oldScore,
NewScore = value
});
}
}
}
Advanced Service Patterns
Service Composition
public interface IAudioManager
{
Task PlayUISound(string soundId);
Task PlayGameSound(string soundId, Vector3 position);
void SetMasterVolume(float volume);
}
public class AudioManager : IAudioManager
{
private readonly ISoundPlayer _soundPlayer;
private readonly ISettingsService _settings;
private readonly ILocalizationService _localization;
private readonly Dictionary<string, string> _soundPaths;
public AudioManager(
ISoundPlayer soundPlayer,
ISettingsService settings,
ILocalizationService localization)
{
_soundPlayer = soundPlayer;
_settings = settings;
_localization = localization;
_soundPaths = new Dictionary<string, string>
{
{"button-click", "assets/sounds/ui/button-click.wav"},
{"menu-hover", "assets/sounds/ui/menu-hover.wav"},
{"explosion", "assets/sounds/game/explosion.wav"},
{"pickup", "assets/sounds/game/pickup.wav"}
};
}
public async Task PlayUISound(string soundId)
{
if (_soundPaths.TryGetValue(soundId, out string path))
{
var uiVolume = _settings.GetSetting<float>("UIVolume", 0.8f);
await _soundPlayer.PlaySound(path, uiVolume);
}
else
{
Console.WriteLine($"UI sound '{soundId}' not found");
}
}
public async Task PlayGameSound(string soundId, Vector3 position)
{
if (_soundPaths.TryGetValue(soundId, out string path))
{
var gameVolume = _settings.GetSetting<float>("GameVolume", 1.0f);
// Calculate 3D audio volume based on distance
// (simplified implementation)
var cameraPosition = GetCameraPosition();
var distance = Vector3.Distance(position, cameraPosition);
var adjustedVolume = gameVolume * Math.Max(0, 1 - (distance / 100f));
await _soundPlayer.PlaySound(path, (float)adjustedVolume);
}
else
{
Console.WriteLine($"Game sound '{soundId}' not found");
}
}
public void SetMasterVolume(float volume)
{
_soundPlayer.SetSoundVolume(volume);
_soundPlayer.SetMusicVolume(volume * 0.7f); // Music slightly quieter
_settings.SetSetting("MasterVolume", volume);
}
private Vector3 GetCameraPosition()
{
// Get camera position from current scene
var sceneManager = Game.Instance.SceneManager;
var currentScene = sceneManager.GetCurrentScene();
if (currentScene is Scene3D scene3D)
{
return scene3D.GetCameraPosition();
}
return Vector3.Zero;
}
}
Service Lifecycle Management
public interface IServiceLifecycle
{
Task InitializeAsync();
Task ShutdownAsync();
bool IsInitialized { get; }
}
public abstract class ManagedService : IServiceLifecycle
{
protected bool _isInitialized = false;
public bool IsInitialized => _isInitialized;
public virtual async Task InitializeAsync()
{
if (!_isInitialized)
{
await OnInitializeAsync();
_isInitialized = true;
}
}
public virtual async Task ShutdownAsync()
{
if (_isInitialized)
{
await OnShutdownAsync();
_isInitialized = false;
}
}
protected abstract Task OnInitializeAsync();
protected abstract Task OnShutdownAsync();
}
public class DatabaseService : ManagedService, IDatabaseService
{
private SqliteConnection _connection;
protected override async Task OnInitializeAsync()
{
// Initialize database connection
_connection = new SqliteConnection("Data Source=game.db");
await _connection.OpenAsync();
// Create tables if they don't exist
await CreateTablesAsync();
Console.WriteLine("Database service initialized");
}
protected override async Task OnShutdownAsync()
{
if (_connection != null)
{
await _connection.CloseAsync();
_connection.Dispose();
_connection = null;
}
Console.WriteLine("Database service shutdown");
}
private async Task CreateTablesAsync()
{
var createPlayerTable = @"
CREATE TABLE IF NOT EXISTS Players (
Id TEXT PRIMARY KEY,
Name TEXT NOT NULL,
Level INTEGER DEFAULT 1,
Score INTEGER DEFAULT 0,
CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
)";
using var command = new SqliteCommand(createPlayerTable, _connection);
await command.ExecuteNonQueryAsync();
}
}
// Service manager for lifecycle coordination
public class ServiceManager
{
private readonly List<IServiceLifecycle> _managedServices = new();
public void RegisterManagedService(IServiceLifecycle service)
{
_managedServices.Add(service);
}
public async Task InitializeAllServicesAsync()
{
Console.WriteLine("Initializing managed services...");
foreach (var service in _managedServices)
{
try
{
await service.InitializeAsync();
Console.WriteLine($"Initialized {service.GetType().Name}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to initialize {service.GetType().Name}: {ex.Message}");
}
}
Console.WriteLine("Service initialization complete");
}
public async Task ShutdownAllServicesAsync()
{
Console.WriteLine("Shutting down managed services...");
// Shutdown in reverse order
for (int i = _managedServices.Count - 1; i >= 0; i--)
{
try
{
await _managedServices[i].ShutdownAsync();
Console.WriteLine($"Shutdown {_managedServices[i].GetType().Name}");
}
catch (Exception ex)
{
Console.WriteLine($"Error shutting down {_managedServices[i].GetType().Name}: {ex.Message}");
}
}
Console.WriteLine("Service shutdown complete");
}
}
Service Testing
Mock Service Implementation
public class MockSoundPlayer : ISoundPlayer
{
public List<string> PlayedSounds { get; } = new();
public List<string> PlayedMusic { get; } = new();
public float SoundVolume { get; private set; } = 1.0f;
public float MusicVolume { get; private set; } = 1.0f;
public Task PlaySound(string soundPath, float volume = 1.0f)
{
PlayedSounds.Add(soundPath);
return Task.CompletedTask;
}
public Task PlayMusic(string musicPath, bool loop = true)
{
PlayedMusic.Add(musicPath);
return Task.CompletedTask;
}
public void StopMusic()
{
// Mock implementation - just clear played music
PlayedMusic.Clear();
}
public void SetMusicVolume(float volume)
{
MusicVolume = volume;
}
public void SetSoundVolume(float volume)
{
SoundVolume = volume;
}
public void Dispose()
{
// Mock cleanup
PlayedSounds.Clear();
PlayedMusic.Clear();
}
}
// Test example
[Test]
public async Task TestGameAudio()
{
// Arrange
var mockSoundPlayer = new MockSoundPlayer();
var gameLogic = new GameLogic(mockSoundPlayer);
// Act
await gameLogic.HandleButtonClick();
// Assert
Assert.Contains("assets/sounds/button-click.wav", mockSoundPlayer.PlayedSounds);
Assert.AreEqual(0.8f, mockSoundPlayer.SoundVolume);
}
Service Integration Testing
public class ServiceIntegrationTest
{
private Game testGame;
private ISoundPlayer soundPlayer;
private ISettingsService settingsService;
[SetUp]
public void Setup()
{
// Create test game instance
testGame = new TestGame();
testGame.Load();
// Get services for testing
soundPlayer = testGame.GetService<ISoundPlayer>();
settingsService = testGame.GetService<ISettingsService>();
}
[Test]
public async Task TestServiceInteraction()
{
// Test that settings affect sound service
settingsService.SetSetting("SoundVolume", 0.5f);
soundPlayer.SetSoundVolume(settingsService.GetSetting<float>("SoundVolume", 1.0f));
// Verify the interaction worked
await soundPlayer.PlaySound("test.wav");
// Additional verification logic...
}
[TearDown]
public void Cleanup()
{
testGame?.CleanupResources();
}
}
Performance Considerations
Service Optimization
public class OptimizedAudioService : ISoundPlayer
{
private readonly Dictionary<string, Sound> _cachedSounds = new();
private readonly object _cacheLock = new object();
private readonly int _maxCachedSounds = 50;
public async Task PlaySound(string soundPath, float volume = 1.0f)
{
Sound sound;
lock (_cacheLock)
{
if (!_cachedSounds.TryGetValue(soundPath, out sound))
{
// Load and cache the sound
sound = Raylib.LoadSound(soundPath);
// Manage cache size
if (_cachedSounds.Count >= _maxCachedSounds)
{
var oldestEntry = _cachedSounds.First();
Raylib.UnloadSound(oldestEntry.Value);
_cachedSounds.Remove(oldestEntry.Key);
}
_cachedSounds[soundPath] = sound;
}
}
// Play the cached sound
Raylib.SetSoundVolume(sound, volume);
Raylib.PlaySound(sound);
}
public void Dispose()
{
lock (_cacheLock)
{
foreach (var sound in _cachedSounds.Values)
{
Raylib.UnloadSound(sound);
}
_cachedSounds.Clear();
}
}
}
Lazy Service Loading
public class LazyServiceProvider
{
private readonly Dictionary<Type, Func<object>> _serviceFactories = new();
private readonly Dictionary<Type, object> _serviceInstances = new();
private readonly object _lock = new object();
public void RegisterLazy<T>(Func<T> factory) where T : class
{
_serviceFactories[typeof(T)] = () => factory();
}
public T GetService<T>() where T : class
{
var serviceType = typeof(T);
lock (_lock)
{
if (_serviceInstances.TryGetValue(serviceType, out var instance))
{
return (T)instance;
}
if (_serviceFactories.TryGetValue(serviceType, out var factory))
{
instance = factory();
_serviceInstances[serviceType] = instance;
return (T)instance;
}
}
throw new InvalidOperationException($"Service {serviceType.Name} not registered");
}
}
// Usage
var serviceProvider = new LazyServiceProvider();
serviceProvider.RegisterLazy<IExpensiveService>(() => new ExpensiveService());
// Service is only created when first accessed
var service = serviceProvider.GetService<IExpensiveService>();
Best Practices
Service Design Guidelines
- Single Responsibility: Each service should have a focused, well-defined purpose
- Interface Segregation: Create specific interfaces rather than large, monolithic ones
- Dependency Inversion: Depend on abstractions, not concrete implementations
- Stateless When Possible: Minimize mutable state in services for thread safety
Error Handling
public class RobustNetworkService : INetworkService
{
private readonly ILogger _logger;
private int _retryCount = 0;
private const int MaxRetries = 3;
public async Task<T> SendRequestAsync<T>(string endpoint, object data)
{
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
try
{
return await PerformRequestAsync<T>(endpoint, data);
}
catch (HttpRequestException ex) when (attempt < MaxRetries)
{
_logger.LogWarning($"Request attempt {attempt + 1} failed: {ex.Message}");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
catch (Exception ex)
{
_logger.LogError($"Request failed permanently: {ex.Message}");
throw;
}
}
throw new InvalidOperationException("Max retries exceeded");
}
}
Resource Management
public class ResourceAwareService : IDisposable
{
private readonly List<IDisposable> _resources = new();
private bool _disposed = false;
protected void TrackResource(IDisposable resource)
{
_resources.Add(resource);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
foreach (var resource in _resources)
{
try
{
resource?.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"Error disposing resource: {ex.Message}");
}
}
_resources.Clear();
}
_disposed = true;
}
}
}
Integration with Game Architecture
Service-Scene Integration
public class GameplayScene : Scene
{
private readonly IAudioManager _audioManager;
private readonly IGameStateService _gameState;
private readonly IPlayerService _playerService;
public GameplayScene()
{
// Inject services through Game instance
_audioManager = Game.Instance.GetService<IAudioManager>();
_gameState = Game.Instance.GetService<IGameStateService>();
_playerService = Game.Instance.GetService<IPlayerService>();
}
protected override void OnLoad()
{
// Subscribe to service events
_gameState.OnScoreChanged += HandleScoreChanged;
// Initialize scene with service data
var currentPlayer = _playerService.GetCurrentPlayer();
var playerNameLabel = GetElement<Label>("playerNameLabel");
playerNameLabel.SetText(currentPlayer.Name);
// Play scene music
_ = _audioManager.PlayUISound("scene-enter");
}
private async void HandleScoreChanged(int newScore)
{
await _audioManager.PlayUISound("score-change");
var scoreLabel = GetElement<Label>("scoreLabel");
scoreLabel.SetText($"Score: {newScore}");
}
protected override void OnUnload()
{
// Unsubscribe from service events
_gameState.OnScoreChanged -= HandleScoreChanged;
base.OnUnload();
}
}
Service Documentation Examples
Each service should be thoroughly documented with usage examples. Here are links to the detailed service documentation:
- Audio System - Sound and music playback
- Settings Management - Configuration and preferences
- Localization - Multi-language support
- Discord Integration - Discord Rich Presence
- Asset Registry - Resource management
- Input System - Input handling and management
Custom Service Creation
Creating a New Service
- Define the Interface
public interface IAnalyticsService
{
void TrackEvent(string eventName, Dictionary<string, object> parameters = null);
void TrackScreen(string screenName);
void SetUserProperty(string name, string value);
Task FlushEventsAsync();
}
- Implement the Service
public class AnalyticsService : IAnalyticsService
{
// Implementation details...
}
- Register with Game
public override void Load()
{
var analyticsService = new AnalyticsService();
RegisterService<IAnalyticsService>(analyticsService);
}
- Use in Your Code
var analytics = Game.Instance.GetService<IAnalyticsService>();
analytics.TrackEvent("level_completed", new Dictionary<string, object>
{
{"level", 5},
{"score", 1250},
{"time", 120.5}
});
The service architecture provides a flexible foundation for building scalable, maintainable games while promoting good software engineering practices throughout your codebase.
Further Reading
- Game Architecture - Overall engine architecture patterns
- Scene Management - Scene system integration with services
- Advanced Features - Service-enabled advanced functionality
- Examples - Practical service usage examples