/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Audio { using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; /// /// The AudioManager manages the audio to ensure to ensure no two clips are playing on the same AudioSource at the same time. /// public class AudioManager : MonoBehaviour { private static AudioManager s_Instance; private static AudioManager Instance { get { if (!s_Initialized) { s_Instance = new GameObject("Audio Manager").AddComponent(); s_Initialized = true; } return s_Instance; } } private static bool s_Initialized; /// /// The AudioSourcesIndex class allows for an AudioSource to be selected based upon its play state. If all AudioSources are being /// played then a new AudioSource will be added. /// private class AudioSourcesIndex { private GameObject m_GameObject; private AudioSource[] m_AudioSources; private int m_ReserveCount = 0; private int m_Index; private bool m_AudioManagerGameObject; public AudioSource[] AudioSources { get { return m_AudioSources; } } public int ReserveCount { set { m_ReserveCount = value; } } /// /// AudioSourcesIndex constructor. /// /// The GameObject that the AudioSources are attached to. /// Is this object attached to the AudioManager? /// The volume of the new audio source. public AudioSourcesIndex(GameObject gameObject, bool audioManagerGameObject, float volume) { m_GameObject = gameObject; m_AudioManagerGameObject = audioManagerGameObject; m_AudioSources = m_GameObject.GetComponents(); // At least one AudioSource must exist. if (m_AudioSources.Length == 0) { AddAudioSource(volume); } else { for (int i = 0; i < m_AudioSources.Length; ++i) { m_AudioSources[i].playOnAwake = false; } } } /// /// Returns an AudioSource which is not currently playing. /// /// The index of the component that should be played. -1 indicates any component. /// An AudioSource which is not currently playing. public AudioSource GetAvailableAudioSource(int reservedIndex) { return GetAvailableAudioSource(reservedIndex, m_AudioSources[m_Index], false); } /// /// Returns an AudioSource which is not currently playing. /// /// The index of the component that should be played. -1 indicates any component. /// Specifies which AudioSource to copy from if the properties need to be copied. /// Should the AudioSource properties be copied even if a new AudioSource isn't created? /// An AudioSource which is not currently playing. public AudioSource GetAvailableAudioSource(int reservedIndex, AudioSource copyFromAudioSource, bool forceCopyProperties) { // If the same AudioSource is requested it allows the audio to be interrupted (such as for equip/unequip). AudioSource audioSource = null; if (reservedIndex != -1) { while (reservedIndex >= m_AudioSources.Length) { AddAudioSource(1); } if (forceCopyProperties) { CopyAudioProperties(copyFromAudioSource, m_AudioSources[reservedIndex]); } return m_AudioSources[reservedIndex]; } else if (m_Index < m_ReserveCount) { // Ensure the index doesn't occupy an element that is reserved. m_Index = m_ReserveCount % m_AudioSources.Length; if (m_Index < m_ReserveCount) { // If the index is still less then the reserve count then a new AudioSource needs to be created. audioSource = AddAudioSource(1); CopyAudioProperties(copyFromAudioSource, audioSource); m_Index = m_AudioSources.Length - 1; } } else { audioSource = m_AudioSources[m_Index]; } var count = 0; var copyProperties = forceCopyProperties; while (audioSource.isPlaying) { if (count < m_AudioSources.Length && audioSource != null) { m_Index = (m_Index + 1) % m_AudioSources.Length; // The AudioSource is resrved and cannot be used if the index is less then the min reserved index. This allows AudioSources // to be designated for a specific effect. if (m_Index < m_ReserveCount) { m_Index = m_ReserveCount % m_AudioSources.Length; // If the index is still less then the Reserve Count then there aren't enough AudioSources available. Set the count to the max // so a new AudioSource will be created. if (m_Index < m_ReserveCount) { count = m_AudioSources.Length; continue; } } audioSource = m_AudioSources[m_Index]; count++; } else { audioSource = AddAudioSource(1); copyProperties = true; } } if (copyProperties) { CopyAudioProperties(copyFromAudioSource, audioSource); } audioSource.spatialBlend = m_AudioManagerGameObject ? 0 : 1; return audioSource; } /// /// Adds a new AudioSource to the array. /// /// The volume of the AudioSource. /// The added AudioSource. private AudioSource AddAudioSource(float volume) { // If the count is equal to the length then a new AudioSource needs to be added. var addGameObject = m_GameObject; // Any child AudioSources of the AudioManager should be attached to their own GameObject so it can be repositioned. if (m_AudioManagerGameObject) { addGameObject = new GameObject("AudioSource"); addGameObject.transform.parent = m_GameObject.transform; } var newAudioSource = addGameObject.AddComponent(); newAudioSource.playOnAwake = false; newAudioSource.volume = volume; if (!m_AudioManagerGameObject) { newAudioSource.spatialBlend = 1; newAudioSource.maxDistance = 20; } // The new AudioSource may be active. newAudioSource.Stop(); // Add the new AudioSource to the array. System.Array.Resize(ref m_AudioSources, m_AudioSources.Length + 1); m_AudioSources[m_AudioSources.Length - 1] = newAudioSource; // Return the new AudioSource. return newAudioSource; } /// /// Copies the AudioSource properties from the original AudioSource to the new AudioSource. /// /// The original AudioSource to copy from. /// The AudioSource to copy to. private void CopyAudioProperties(AudioSource originalAudioSource, AudioSource newAudioSource) { newAudioSource.bypassEffects = originalAudioSource.bypassEffects; newAudioSource.bypassListenerEffects = originalAudioSource.bypassListenerEffects; newAudioSource.bypassReverbZones = originalAudioSource.bypassReverbZones; newAudioSource.dopplerLevel = originalAudioSource.dopplerLevel; newAudioSource.ignoreListenerPause = originalAudioSource.ignoreListenerPause; newAudioSource.ignoreListenerVolume = originalAudioSource.ignoreListenerVolume; newAudioSource.loop = originalAudioSource.loop; newAudioSource.maxDistance = originalAudioSource.maxDistance; newAudioSource.minDistance = originalAudioSource.minDistance; newAudioSource.mute = originalAudioSource.mute; newAudioSource.outputAudioMixerGroup = originalAudioSource.outputAudioMixerGroup; newAudioSource.panStereo = originalAudioSource.panStereo; newAudioSource.playOnAwake = originalAudioSource.playOnAwake; newAudioSource.pitch = originalAudioSource.pitch; newAudioSource.priority = originalAudioSource.priority; newAudioSource.reverbZoneMix = originalAudioSource.reverbZoneMix; newAudioSource.rolloffMode = originalAudioSource.rolloffMode; newAudioSource.spatialBlend = originalAudioSource.spatialBlend; newAudioSource.spatialize = originalAudioSource.spatialize; newAudioSource.spatializePostEffects = originalAudioSource.spatializePostEffects; newAudioSource.spread = originalAudioSource.spread; newAudioSource.velocityUpdateMode = originalAudioSource.velocityUpdateMode; newAudioSource.volume = originalAudioSource.volume; } /// /// Stops playing the AudioSource. /// /// The index of the component that should be stopped. -1 indicates all components. public void Stop(int reservedIndex) { if (reservedIndex == -1) { for (int i = 0; i < m_AudioSources.Length; ++i) { m_AudioSources[i].Stop(); } } else { m_AudioSources[reservedIndex].Stop(); } } } private Dictionary m_GameObjectAudioSourcesMap = new Dictionary(); private GameObject m_GameObject; /// /// The object has been enabled. /// private void OnEnable() { // The object may have been enabled outside of the scene unloading. if (s_Instance == null) { s_Instance = this; s_Initialized = true; SceneManager.sceneUnloaded -= SceneUnloaded; } } /// /// The AudioManager can also play audio clips if the target GameObject is being disabled. /// private void Start() { m_GameObject = gameObject; RegisterInternal(m_GameObject, 1); } /// /// Registers the AudioSources on the GameObject so they can be played. /// /// The GameObject to register. public static void Register(GameObject gameObject) { Instance.RegisterInternal(gameObject, 1); } /// /// Registers the AudioSources on the GameObject so they can be played. /// /// The GameObject to register. /// The volume of the new audio source. public static void Register(GameObject gameObject, float volume) { Instance.RegisterInternal(gameObject, volume); } /// /// Internal method which registers the AudioSources on the GameObject so they can be played. /// /// The GameObject to register. /// The volume of the new audio source. protected virtual void RegisterInternal(GameObject gameObject, float volume) { // The same GameObject can act as an AudioSource for multiple objects so it may have already been registered. if (m_GameObjectAudioSourcesMap.ContainsKey(gameObject)) { return; } var audioSourcesIndex = new AudioSourcesIndex(gameObject, gameObject == m_GameObject, volume); m_GameObjectAudioSourcesMap.Add(gameObject, audioSourcesIndex); } /// /// Sets the number of components that should be reserved for a specific effect. /// /// The GameObject that is reserving the components. /// The number of components to reserve. public static void SetReserveCount(GameObject gameObject, int count) { Instance.SetReserveCountInternal(gameObject, count); } /// /// Internal method which sets the number of components that should be reserved for a specific effect. /// /// The GameObject that is reserving the components. /// The number of components to reserve. public void SetReserveCountInternal(GameObject gameObject, int count) { AudioSourcesIndex audioSourcesIndex; if (!m_GameObjectAudioSourcesMap.TryGetValue(gameObject, out audioSourcesIndex)) { Debug.LogError("Error: The GameObject " + gameObject.name + " has not been registered with the AudioManager."); return; } audioSourcesIndex.ReserveCount = count; } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip) { return Instance.PlayInternal(gameObject, clip, 1, false, 0, -1); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// Does the clip loop? /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, bool loop) { return Instance.PlayInternal(gameObject, clip, 1, loop, 0, -1); } /// /// Plays the audio clip with the specified delay. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The number of seconds to delay the clip from playing. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource PlayDelayed(GameObject gameObject, AudioClip clip, float delay) { return Instance.PlayInternal(gameObject, clip, 1, false, delay, -1); } /// /// Plays the audio clip with the specified delay. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// Does the clip loop? /// The number of seconds to delay the clip from playing. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource PlayDelayed(GameObject gameObject, AudioClip clip, float delay, bool loop) { return Instance.PlayInternal(gameObject, clip, 1, loop, delay, -1); } /// /// Plays the audio clip with the specified delay. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The number of seconds to delay the clip from playing. /// The index of the component that should be played. -1 indicates any component. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource PlayDelayed(GameObject gameObject, AudioClip clip, float delay, int reservedIndex) { return Instance.PlayInternal(gameObject, clip, 1, false, delay, reservedIndex); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch) { return Instance.PlayInternal(gameObject, clip, pitch, false, 0, -1); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// Does the clip loop? /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch, bool loop) { return Instance.PlayInternal(gameObject, clip, pitch, loop, 0, -1); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The number of seconds to delay the clip from playing. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch, float delay) { return Instance.PlayInternal(gameObject, clip, pitch, false, delay, -1); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The number of seconds to delay the clip from playing. /// The index of the component that should be played. -1 indicates any component. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch, float delay, int reservedIndex) { return Instance.PlayInternal(gameObject, clip, pitch, false, delay, reservedIndex); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// Does the clip loop? /// The number of seconds to delay the clip from playing. /// The index of the component that should be played. -1 indicates any component. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch, bool loop, float delay, int reservedIndex) { return Instance.PlayInternal(gameObject, clip, pitch, loop, delay, reservedIndex); } /// /// Plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// The index of the component that should be played. -1 indicates any component. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource Play(GameObject gameObject, AudioClip clip, float pitch, int reservedIndex) { return Instance.PlayInternal(gameObject, clip, pitch, false, 0, reservedIndex); } /// /// Internal method which plays the audio clip. /// /// The GameObject that is playing the audio clip. /// The clip to play. /// The pitch to play the clip at. /// Does the clip loop? /// The number of seconds to delay the clip from playing. /// The index of the component that should be played. -1 indicates any component. /// The AudioSource that is playing the AudioClip (can be null). protected virtual AudioSource PlayInternal(GameObject gameObject, AudioClip clip, float pitch, bool loop, float delay, int reservedIndex) { if (clip == null) { return null; } AudioSourcesIndex audioSourcesIndex; AudioSource audioSource = null; if (gameObject != null && gameObject.activeInHierarchy) { if (!m_GameObjectAudioSourcesMap.TryGetValue(gameObject, out audioSourcesIndex)) { RegisterInternal(gameObject, 1); audioSourcesIndex = m_GameObjectAudioSourcesMap[gameObject]; } audioSource = audioSourcesIndex.GetAvailableAudioSource(reservedIndex); } else { // If a GameObject is disabled then it can't play the AudioSource. Use the scene AudioSource which will always be active. var sceneAudioSourcesIndex = m_GameObjectAudioSourcesMap[m_GameObject]; audioSource = sceneAudioSourcesIndex.GetAvailableAudioSource(reservedIndex, sceneAudioSourcesIndex.AudioSources[0], true); // The position may have been changed by PlayAtPosition. audioSource.transform.localPosition = Vector3.zero; } // Play the clip. audioSource.clip = clip; audioSource.pitch = pitch; audioSource.loop = loop; if (delay == 0) { audioSource.Play(); } else { audioSource.PlayDelayed(delay); } return audioSource; } /// /// Plays the audio clip at the specified position. /// /// The clip that should be played. /// The position that the clip should be played at. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource PlayAtPosition(AudioClip clip, Vector3 position) { return Instance.PlayAtPositionInternal(clip, position, 1, 1); } /// /// Plays the audio clip at the specified position. /// /// The clip that should be played. /// The position that the clip should be played at. /// The volume to play the clip at. /// The pitch to play the clip at. /// The AudioSource that is playing the AudioClip (can be null). public static AudioSource PlayAtPosition(AudioClip clip, Vector3 position, float volume, float pitch) { return Instance.PlayAtPositionInternal(clip, position, volume, pitch); } /// /// Internal method which plays the audio clip at the specified position. /// /// The clip that should be played. /// The position that the clip should be played at. /// The volume to play the clip at. /// The pitch to play the clip at. /// The AudioSource that is playing the AudioClip (can be null). protected virtual AudioSource PlayAtPositionInternal(AudioClip clip, Vector3 position, float volume, float pitch) { var sceneAudioSourcesIndex = m_GameObjectAudioSourcesMap[m_GameObject]; var audioSource = sceneAudioSourcesIndex.GetAvailableAudioSource(-1, sceneAudioSourcesIndex.AudioSources[0], true); audioSource.transform.position = position; // Play the clip. audioSource.loop = false; audioSource.clip = clip; audioSource.volume = volume; audioSource.pitch = pitch; audioSource.spatialBlend = 1; audioSource.maxDistance = 500; audioSource.Play(); return audioSource; } /// /// Stops any playing audio on the specified GameObject. /// /// The GameObject to stop the audio on. public static void Stop(GameObject gameObject) { Instance.StopInternal(gameObject, -1); } /// /// Stops any playing audio on the specified GameObject. /// /// The GameObject to stop the audio on. /// The index of the component that should be stopped. -1 indicates all components. public static void Stop(GameObject gameObject, int reservedIndex) { Instance.StopInternal(gameObject, reservedIndex); } /// /// Internal method which stops any playing audio on the specified GameObject. /// /// The GameObject to stop the audio on. /// The index of the component that should be stopped. -1 indicates all components. private void StopInternal(GameObject gameObject, int reservedIndex) { AudioSourcesIndex audioSourcesIndex; if (!m_GameObjectAudioSourcesMap.TryGetValue(gameObject, out audioSourcesIndex)) { return; } audioSourcesIndex.Stop(reservedIndex); } /// /// Reset the initialized variable when the scene is no longer loaded. /// /// The scene that was unloaded. private void SceneUnloaded(Scene scene) { s_Initialized = false; s_Instance = null; SceneManager.sceneUnloaded -= SceneUnloaded; } /// /// The object has been disabled. /// private void OnDisable() { SceneManager.sceneUnloaded += SceneUnloaded; } #if UNITY_2019_3_OR_NEWER /// /// Reset the static variables for domain reloading. /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void DomainReset() { s_Initialized = false; s_Instance = null; } #endif } }