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);
}
}
}
}