using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; namespace OM { /// /// Manages sound playback within the application using a Singleton pattern. /// Handles playing sounds via pooled OM_AudioPlayer instances for efficiency /// and also provides methods for playing simple one-shot sounds directly /// using its own AudioSource component. /// [RequireComponent(typeof(AudioSource))] // Ensures this manager GameObject always has an AudioSource for PlayOneShot sounds. public class OM_SoundManager : MonoBehaviour { // --- Constants for Resource Loading --- /// /// The path within the Resources folder where the OM_SoundManager prefab is located. /// Used by the Singleton pattern if no instance exists and it needs to be instantiated. /// private const string PrefabPathInResources = "OM_SoundManager"; /// /// The path within the Resources folder where the OM_AudioPlayer prefab is located. /// Used to instantiate new audio players for the object pool. /// private const string AudioPlayerPrefabInResources = "OM_AudioPlayer"; // --- Singleton Implementation --- /// /// The static backing field for the Singleton instance. /// private static OM_SoundManager _instance; /// /// Provides global access to the single OM_SoundManager instance. /// Handles finding an existing instance, loading it from Resources, or creating a new one if necessary. /// Ensures the manager is initialized via the Init() method. /// public static OM_SoundManager Instance { get { // 1. Check if the instance already exists. if (_instance != null) return _instance; // 2. If not, try to find an existing OM_SoundManager in the scene. // Note: FindFirstObjectByType is generally preferred over FindObjectOfType in modern Unity. _instance = FindFirstObjectByType(); if (_instance != null) { // Found an existing instance, ensure it's initialized and return it. if (!_instance._initialized) _instance.Init(); return _instance; } // 3. If no instance exists in the scene, try loading the prefab from Resources. var prefab = Resources.Load(PrefabPathInResources); if (prefab != null) { // Instantiate the prefab, initialize it, and return the new instance. _instance = Instantiate(prefab); _instance.Init(); // Init is called *after* instantiation. return _instance; } // 4. If no prefab was found, create a new GameObject and add the OM_SoundManager component. // This is a fallback scenario. Debug.LogWarning($"OM_SoundManager prefab not found at Resources/{PrefabPathInResources}. Creating a new instance dynamically."); _instance = new GameObject("OM_SoundManager").AddComponent(); _instance.Init(); // Init is called *after* component creation. return _instance; } } // --- Object Pooling --- /// /// The object pool responsible for managing OM_AudioPlayer instances. /// This improves performance by reusing player objects instead of constantly creating and destroying them. /// public ObjectPool AudioPlayerPool { get; private set; } /// /// Flag to prevent redundant initialization. /// private bool _initialized = false; /// /// The AudioSource component attached to the SoundManager itself. /// Used primarily for PlayOneShot sounds that don't require pooling or complex control. /// private AudioSource _audioSource; /// /// Cached reference to the loaded OM_AudioPlayer prefab. /// private OM_AudioPlayer _audioPlayerPrefab; /// /// A list containing references to all currently active (borrowed from the pool) OM_AudioPlayer instances. /// Used by the Update loop to check when players finish playing. /// private readonly List _activePlayers = new List(); // --- Initialization --- /// /// Initializes the Sound Manager. This is called automatically by the Instance getter /// when the instance is first accessed or created. /// Sets up the manager's AudioSource, DontDestroyOnLoad, loads the player prefab, /// and configures the ObjectPool. /// private void Init() { if (_initialized) return; // Prevent multiple initializations. _initialized = true; // Get or add the manager's own AudioSource component. _audioSource = GetComponent(); if (_audioSource == null) _audioSource = gameObject.AddComponent(); // Configure the manager's AudioSource for one-shot sounds (non-looping, doesn't play on awake). _audioSource.playOnAwake = false; _audioSource.loop = false; // Ensure the Sound Manager persists across scene loads. DontDestroyOnLoad(gameObject); // Load the OM_AudioPlayer prefab from Resources. _audioPlayerPrefab = Resources.Load(AudioPlayerPrefabInResources); if (_audioPlayerPrefab == null) { // Log an error if the prefab is missing, as pooling won't work. Debug.LogError($"AudioPlayer prefab not found in Resources at path: {AudioPlayerPrefabInResources}"); // Potentially disable pooling or throw an exception here depending on desired robustness. } // Create and configure the ObjectPool for OM_AudioPlayer instances. AudioPlayerPool = new ObjectPool( createFunc: () => // Action to create a new player instance when the pool is empty. { if (_audioPlayerPrefab == null) { Debug.LogError("Cannot create new AudioPlayer, prefab is missing!"); return null; // Or handle this error more gracefully } // Instantiate the prefab as a child of the SoundManager. var audioPlayer = Instantiate(_audioPlayerPrefab, transform); audioPlayer.Setup(this); // Initialize the new player instance. return audioPlayer; }, actionOnGet: OnAudioPlayerGet, // Action called when an item is taken from the pool. actionOnRelease: OnAudioPlayerRelease, // Action called when an item is returned to the pool. actionOnDestroy: OnAudioPlayerDestroy, // Action called when an item is destroyed (e.g., if pool capacity is exceeded). collectionCheck: true, // Enable checks to prevent returning an item multiple times. defaultCapacity: 10, // Initial capacity (adjust as needed). maxSize: 50 // Maximum number of players the pool will hold (adjust as needed). ); } // --- Pool Callbacks --- /// /// Called when an OM_AudioPlayer is destroyed by the pool (e.g., if maxSize is exceeded). /// Currently logs a warning but could be used for cleanup if necessary. /// /// The player instance being destroyed. private void OnAudioPlayerDestroy(OM_AudioPlayer player) { // Ensure the GameObject is actually destroyed if the pool decides to remove it. if (player != null && player.gameObject != null) { Destroy(player.gameObject); } // Debug.LogWarning($"Destroying AudioPlayer: {player.name}"); // Optional: Log destruction } /// /// Called when an OM_AudioPlayer is returned to the object pool. /// Deactivates the player's GameObject and removes it from the active players list. /// /// The player instance being released. private void OnAudioPlayerRelease(OM_AudioPlayer player) { player.gameObject.SetActive(false); // Hide the player. _activePlayers.Remove(player); // Remove from tracking. // Optional: Reset parent if necessary, though Instantiate in createFunc sets it initially. // player.transform.SetParent(transform); } /// /// Called when an OM_AudioPlayer is taken from the object pool. /// Activates the player's GameObject and adds it to the active players list for tracking. /// /// The player instance being activated. private void OnAudioPlayerGet(OM_AudioPlayer player) { player.gameObject.SetActive(true); // Make the player visible/active. _activePlayers.Add(player); // Add to tracking. } // --- Playback Methods --- /// /// Plays audio using a pooled OM_AudioPlayer instance based on the provided data. /// Suitable for sounds that might need positioning or longer playback duration. /// /// The structured data containing the AudioClip, volume, pitch, ID, and optional position. public void Play(OM_AudioClipData clipData) { // Basic check to ensure the clip is playable. if (!clipData.CanBePlayed()) { Debug.LogWarning($"Attempted to play an invalid AudioClipData (ID: {clipData.Id})."); return; } // Get an OM_AudioPlayer instance from the pool. var player = AudioPlayerPool.Get(); if (player != null) // Check if Get succeeded (it might fail if prefab is null) { player.Play(clipData); // Tell the player instance to play the clip data. } } /// /// Plays a sound immediately using the SoundManager's own AudioSource. /// Does *not* use the object pool. Ideal for short, frequent sounds like UI clicks /// where the overhead of pooling might not be necessary or desired. /// Uses volume from OM_AudioClipData, ignores pitch and position. /// /// The structured data containing the AudioClip and volume. public void PlayOneShot(OM_AudioClipData clipData) { // Basic check to ensure the clip is playable. if (!clipData.CanBePlayed()) { Debug.LogWarning($"Attempted to PlayOneShot an invalid AudioClipData (ID: {clipData.Id})."); return; } // Play using the manager's AudioSource. _audioSource.PlayOneShot(clipData.Clip, clipData.Volume); } /// /// Plays a raw AudioClip immediately using the SoundManager's own AudioSource. /// Does *not* use the object pool. /// /// The raw AudioClip to play. /// The desired volume scale (0.0 to 1.0). public void PlayOneShot(AudioClip clip, float volume = 1) { if (clip == null) { Debug.LogWarning("Attempted to PlayOneShot a null AudioClip."); return; } // Play using the manager's AudioSource. _audioSource.PlayOneShot(clip, volume); } /// /// Plays a sound immediately using the SoundManager's own AudioSource. /// Uses the potentially randomized volume from the OM_AudioClip definition. /// Does *not* use the object pool. Ignores pitch randomization and position. /// /// The OM_AudioClip definition containing the AudioClip and volume/pitch ranges. public void PlayOneShot(OM_AudioClip clip) { // Basic check to ensure the clip is playable. if (!clip.CanBePlayed()) { Debug.LogWarning($"Attempted to PlayOneShot an invalid OM_AudioClip (ID: {clip.Id})."); return; } // Play using the manager's AudioSource, getting a volume value from the definition. _audioSource.PlayOneShot(clip.Clip, clip.GetVolume()); } // --- Utility Methods --- /// /// Retrieves all active OM_AudioPlayer instances currently playing a specific audio ID. /// /// The ID of the audio clip to search for. /// An enumerable collection of matching active OM_AudioPlayer instances. public IEnumerable GetAudioPlayers(int audioId) { // Using yield return creates an iterator block, efficiently returning players one by one. foreach (var activePlayer in _activePlayers) { if (activePlayer.AudioId == audioId) { yield return activePlayer; } } // Alternative using LINQ (potentially less performant due to allocation): // return _activePlayers.Where(player => player.AudioId == audioId); } // --- Update Loop --- /// /// Called every frame by Unity. /// Checks all active OM_AudioPlayer instances and returns any that have finished playing /// back to the object pool. /// private void Update() { // Iterate backwards through the list to safely remove items while iterating. for (var i = _activePlayers.Count - 1; i >= 0; i--) { var player = _activePlayers[i]; // If the player's AudioSource component is still playing, skip it. // Note: isPlaying can sometimes remain true for a frame after sound stops. // More robust checks might involve checking time or clip status if needed. if (player.AudioSource.isPlaying) continue; // If the player is no longer playing, release it back to the pool. // This automatically calls OnAudioPlayerRelease via the pool's configuration. AudioPlayerPool.Release(player); } } } }