345 lines
12 KiB
C#
345 lines
12 KiB
C#
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Runtime.CompilerServices;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
namespace Watermelon
|
|||
|
|
{
|
|||
|
|
public class AudioController
|
|||
|
|
{
|
|||
|
|
private static AudioController instance;
|
|||
|
|
private bool isInitialized;
|
|||
|
|
|
|||
|
|
private List<AudioSourceCase> audioSourcesPool;
|
|||
|
|
|
|||
|
|
private AudioRegistry registry;
|
|||
|
|
private AudioListener audioListener;
|
|||
|
|
private AudioSave save;
|
|||
|
|
|
|||
|
|
// Default 3D audio settings
|
|||
|
|
private readonly float maxDistance;
|
|||
|
|
private readonly float spread;
|
|||
|
|
private readonly AnimationCurve rolloffCurve;
|
|||
|
|
|
|||
|
|
private event OnVolumeChangedCallback volumeChanged;
|
|||
|
|
|
|||
|
|
private Dictionary<AudioType, float> volumeDictionary;
|
|||
|
|
private AudioType[] audioTypes;
|
|||
|
|
|
|||
|
|
// ─── Constructor ─────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// <param name="poolSize">Initial AudioSource pool size.</param>
|
|||
|
|
/// <param name="registry">Optional. Enables GetClip(name) lookups.</param>
|
|||
|
|
/// <param name="maxDistance">Default max distance for 3D sounds.</param>
|
|||
|
|
/// <param name="spread">Default spread for 3D sounds (0–360).</param>
|
|||
|
|
/// <param name="rolloffCurve">Custom rolloff curve. Pass null for linear default.</param>
|
|||
|
|
public AudioController(
|
|||
|
|
int poolSize,
|
|||
|
|
AudioRegistry registry = null,
|
|||
|
|
float maxDistance = 30,
|
|||
|
|
float spread = 180,
|
|||
|
|
AnimationCurve rolloffCurve = null)
|
|||
|
|
{
|
|||
|
|
if (instance != null)
|
|||
|
|
Debug.LogWarning("[AudioController]: Previous instance replaced. Call Unload() before creating a new one.");
|
|||
|
|
|
|||
|
|
instance = this;
|
|||
|
|
|
|||
|
|
this.maxDistance = maxDistance;
|
|||
|
|
this.spread = spread;
|
|||
|
|
this.rolloffCurve = rolloffCurve ?? new AnimationCurve(new Keyframe(0.0f, 1.0f), new Keyframe(1.0f, 0.0f));
|
|||
|
|
this.registry = registry;
|
|||
|
|
|
|||
|
|
registry?.BuildRuntimeLookup();
|
|||
|
|
|
|||
|
|
save = SaveController.GetSaveObject<AudioSave>("audio");
|
|||
|
|
|
|||
|
|
audioTypes = EnumUtils.GetEnumArray<AudioType>();
|
|||
|
|
volumeDictionary = new Dictionary<AudioType, float>();
|
|||
|
|
if (save.VolumeDatas != null)
|
|||
|
|
{
|
|||
|
|
foreach (AudioSave.VolumeData volumeData in save.VolumeDatas)
|
|||
|
|
{
|
|||
|
|
volumeDictionary.Add(volumeData.AudioType, volumeData.Volume);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CreateAudioListener();
|
|||
|
|
|
|||
|
|
audioSourcesPool = new List<AudioSourceCase>();
|
|||
|
|
for (int i = 0; i < poolSize; i++)
|
|||
|
|
{
|
|||
|
|
audioSourcesPool.Add(new AudioSourceCase());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isInitialized = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Static facade: properties ───────────────────────────────────
|
|||
|
|
|
|||
|
|
public static AudioListener AudioListener => instance?.audioListener;
|
|||
|
|
|
|||
|
|
public static bool IsInitialized => instance != null && instance.isInitialized;
|
|||
|
|
|
|||
|
|
// ─── Static facade: event ────────────────────────────────────────
|
|||
|
|
|
|||
|
|
public static event OnVolumeChangedCallback VolumeChanged
|
|||
|
|
{
|
|||
|
|
add
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.volumeChanged += value;
|
|||
|
|
}
|
|||
|
|
remove
|
|||
|
|
{
|
|||
|
|
if (instance != null)
|
|||
|
|
instance.volumeChanged -= value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Static facade: playback ─────────────────────────────────────
|
|||
|
|
|
|||
|
|
public static void PlaySound(AudioClip clip, float volumePercentage = 1.0f, float pitch = 1.0f, float minDelay = 0f)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.PlaySoundInternal(clip, volumePercentage, pitch);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void PlaySound(AudioClip clip, Vector3 position, float volumePercentage = 1.0f, float pitch = 1.0f, float minDelay = 0f)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.PlaySoundInternal(clip, position, volumePercentage, pitch);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Static facade: registry lookup ─────────────────────────────
|
|||
|
|
|
|||
|
|
public static AudioClip GetClip(string name)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return null;
|
|||
|
|
|
|||
|
|
if (instance.registry == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning("[AudioController]: No AudioRegistry assigned. Cannot get clip by name.");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return instance.registry.GetClip(name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Static facade: volume ───────────────────────────────────────
|
|||
|
|
|
|||
|
|
public static float GetVolume(AudioType audioType)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return 1.0f;
|
|||
|
|
return instance.GetVolumeInternal(audioType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void SetVolume(AudioType audioType, float volume)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.SetVolumeInternal(audioType, volume);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static bool IsAudioTypeActive(AudioType audioType)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return false;
|
|||
|
|
return instance.GetVolumeInternal(audioType) == 1.0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Static facade: sources & listener ──────────────────────────
|
|||
|
|
|
|||
|
|
public static void ReleaseSources()
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.ReleaseSourcesInternal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Called by AudioSourceCase during pool creation — no init warning needed.
|
|||
|
|
public static void ApplyDefaultSettings(ref AudioSource audioSource)
|
|||
|
|
{
|
|||
|
|
if (instance == null) return;
|
|||
|
|
instance.ApplyDefaultSettingsInternal(ref audioSource);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static Transform AttachAudioListener(Transform parentObject)
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return null;
|
|||
|
|
return instance.AttachAudioListenerInternal(parentObject);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void ResetAudioListenerParent()
|
|||
|
|
{
|
|||
|
|
if (!CheckInitialized()) return;
|
|||
|
|
instance.ResetAudioListenerParentInternal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private static bool CheckInitialized([CallerMemberName] string caller = "")
|
|||
|
|
{
|
|||
|
|
if (instance == null || !instance.isInitialized)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"[AudioController]: '{caller}' called before initialization.");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Instance implementation ──────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void ApplyDefaultSettingsInternal(ref AudioSource audioSource)
|
|||
|
|
{
|
|||
|
|
audioSource.maxDistance = maxDistance;
|
|||
|
|
audioSource.spread = spread;
|
|||
|
|
audioSource.rolloffMode = AudioRolloffMode.Custom;
|
|||
|
|
audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, rolloffCurve);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void CreateAudioListener()
|
|||
|
|
{
|
|||
|
|
if (audioListener != null)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
GameObject listenerObject = new GameObject("[AUDIO LISTENER]");
|
|||
|
|
listenerObject.transform.position = Vector3.zero;
|
|||
|
|
|
|||
|
|
GameObject.DontDestroyOnLoad(listenerObject);
|
|||
|
|
|
|||
|
|
audioListener = listenerObject.AddComponent<AudioListener>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Transform AttachAudioListenerInternal(Transform parentObject)
|
|||
|
|
{
|
|||
|
|
if (audioListener == null)
|
|||
|
|
CreateAudioListener();
|
|||
|
|
|
|||
|
|
Transform t = audioListener.transform;
|
|||
|
|
t.SetParent(parentObject);
|
|||
|
|
t.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
|
|||
|
|
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ResetAudioListenerParentInternal()
|
|||
|
|
{
|
|||
|
|
if (audioListener == null) return;
|
|||
|
|
|
|||
|
|
audioListener.transform.SetParent(null);
|
|||
|
|
|
|||
|
|
GameObject.DontDestroyOnLoad(audioListener.gameObject);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ReleaseSourcesInternal()
|
|||
|
|
{
|
|||
|
|
foreach (AudioSourceCase sourceCase in audioSourcesPool)
|
|||
|
|
{
|
|||
|
|
if (sourceCase.IsPlaying)
|
|||
|
|
sourceCase.AudioSource.Stop();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void PlaySoundInternal(AudioClip clip, float volumePercentage, float pitch)
|
|||
|
|
{
|
|||
|
|
if (clip == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("[AudioController]: Audio clip is null");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AudioSourceCase sourceCase = GetAudioSource();
|
|||
|
|
|
|||
|
|
AudioSource source = sourceCase.AudioSource;
|
|||
|
|
source.spatialBlend = 0.0f; // 2D sound
|
|||
|
|
source.pitch = pitch;
|
|||
|
|
|
|||
|
|
sourceCase.Play(clip, volumePercentage, AudioType.Sound);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void PlaySoundInternal(AudioClip clip, Vector3 position, float volumePercentage, float pitch)
|
|||
|
|
{
|
|||
|
|
if (clip == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("[AudioController]: Audio clip is null");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AudioSourceCase sourceCase = GetAudioSource();
|
|||
|
|
|
|||
|
|
AudioSource source = sourceCase.AudioSource;
|
|||
|
|
source.transform.position = position;
|
|||
|
|
source.spatialBlend = 1.0f; // 3D sound
|
|||
|
|
source.pitch = pitch;
|
|||
|
|
|
|||
|
|
sourceCase.Play(clip, volumePercentage, AudioType.Sound);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private AudioSourceCase GetAudioSource()
|
|||
|
|
{
|
|||
|
|
foreach (AudioSourceCase audioSource in audioSourcesPool)
|
|||
|
|
{
|
|||
|
|
if (!audioSource.IsPlaying)
|
|||
|
|
return audioSource;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
AudioSourceCase createdSource = new AudioSourceCase();
|
|||
|
|
audioSourcesPool.Add(createdSource);
|
|||
|
|
|
|||
|
|
return createdSource;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private float GetVolumeInternal(AudioType audioType)
|
|||
|
|
{
|
|||
|
|
if (volumeDictionary.ContainsKey(audioType))
|
|||
|
|
return volumeDictionary[audioType];
|
|||
|
|
|
|||
|
|
return 1.0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void SetVolumeInternal(AudioType audioType, float volume)
|
|||
|
|
{
|
|||
|
|
foreach (AudioSourceCase audioSource in audioSourcesPool)
|
|||
|
|
{
|
|||
|
|
audioSource.OverrideVolume(audioType, volume);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
volumeDictionary[audioType] = volume;
|
|||
|
|
|
|||
|
|
FlushVolumesToSave();
|
|||
|
|
SaveController.MarkAsSaveIsRequired();
|
|||
|
|
|
|||
|
|
volumeChanged?.Invoke(audioType, volume);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void FlushVolumesToSave()
|
|||
|
|
{
|
|||
|
|
save.VolumeDatas = new AudioSave.VolumeData[audioTypes.Length];
|
|||
|
|
for (int i = 0; i < audioTypes.Length; i++)
|
|||
|
|
save.VolumeDatas[i] = new AudioSave.VolumeData { AudioType = audioTypes[i], Volume = GetVolumeInternal(audioTypes[i]) };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Unload()
|
|||
|
|
{
|
|||
|
|
if (save != null && volumeDictionary != null)
|
|||
|
|
FlushVolumesToSave();
|
|||
|
|
|
|||
|
|
audioSourcesPool = null;
|
|||
|
|
registry = null;
|
|||
|
|
audioListener = null;
|
|||
|
|
save = null;
|
|||
|
|
volumeDictionary = null;
|
|||
|
|
audioTypes = null;
|
|||
|
|
volumeChanged = null;
|
|||
|
|
isInitialized = false;
|
|||
|
|
instance = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public delegate void OnVolumeChangedCallback(AudioType audioType, float volume);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum AudioType
|
|||
|
|
{
|
|||
|
|
Music = 0,
|
|||
|
|
Sound = 1
|
|||
|
|
}
|
|||
|
|
}
|