/// ---------------------------------------------
/// 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
}
}