Popcorning Guinea Pig
Search Results for

    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

    1. Single Responsibility: Each service should have a focused, well-defined purpose
    2. Interface Segregation: Create specific interfaces rather than large, monolithic ones
    3. Dependency Inversion: Depend on abstractions, not concrete implementations
    4. 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

    1. 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();
    }
    
    1. Implement the Service
    public class AnalyticsService : IAnalyticsService
    {
        // Implementation details...
    }
    
    1. Register with Game
    public override void Load()
    {
        var analyticsService = new AnalyticsService();
        RegisterService<IAnalyticsService>(analyticsService);
    }
    
    1. 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
    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.