Danro/Assets/Scripts/AudioManager.cs

373 lines
14 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
using Random = UnityEngine.Random;
namespace RR.Common.Managers
{
public class AudioManager : MonoBehaviour
{
// ==== Singleton ====
public static AudioManager Instance { get; private set; }
[Header("Music")]
[SerializeField] private AudioSource musicSource; // child: MusicSource
[SerializeField] private AudioSource tempMusicSource; // child: TempMusicSource
[Tooltip("Группа микшера для музыки (необязательно)")]
[SerializeField] private AudioMixerGroup musicMixerGroup;
[Tooltip("Целевой громкость музыки (после фейдов)")]
[Range(0f, 1.5f)] public float musicTargetVolume = 1f;
[Tooltip("Длительность фейда по умолчанию (сек)")]
[SerializeField] private float defaultFadeDuration = 0.8f;
[Header("SFX Pool")]
[SerializeField] private int sfxPoolSize = 8;
[SerializeField] private Transform sfxPoolParent; // child: SFX Pool
[Tooltip("Группа микшера для SFX (необязательно)")]
[SerializeField] private AudioMixerGroup sfxMixerGroup;
[Range(0f, 1.5f)] public float sfxDefaultVolume = 1f;
private readonly List<AudioSource> sfxPool = new List<AudioSource>(64);
private int sfxRoundRobinIndex = 0;
// Внутреннее
private Coroutine musicFadeCoroutine;
private Coroutine crossfadeCoroutine;
private float storedMusicVolumeBeforePause = 1f;
private bool isPausedFaded = false;
#region Unity Lifetime
public void Initialize()
{
// Singleton init
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
EnsureMusicSources();
EnsureSfxPool();
}
//private void Awake()
//{
//}
// Создаёт/находит MusicSource и TempMusicSource
private void EnsureMusicSources()
{
if (musicSource == null)
{
var t = transform.Find("MusicSource");
if (t != null) musicSource = t.GetComponent<AudioSource>();
}
if (tempMusicSource == null)
{
var t = transform.Find("TempMusicSource");
if (t != null) tempMusicSource = t.GetComponent<AudioSource>();
}
if (musicSource == null) musicSource = CreateChildAudioSource("MusicSource", loop: true, output: musicMixerGroup);
if (tempMusicSource == null) tempMusicSource = CreateChildAudioSource("TempMusicSource", loop: true, output: musicMixerGroup);
PrepareMusicSource(musicSource);
PrepareMusicSource(tempMusicSource);
tempMusicSource.volume = 0f; // запасной источник молчит
musicSource.volume = Mathf.Clamp01(musicTargetVolume);
}
private void PrepareMusicSource(AudioSource src)
{
src.playOnAwake = false;
src.loop = true;
src.spatialBlend = 0f; // 2D музыка
if (musicMixerGroup != null) src.outputAudioMixerGroup = musicMixerGroup;
}
private AudioSource CreateChildAudioSource(string childName, bool loop, AudioMixerGroup output)
{
var go = new GameObject(childName);
go.transform.SetParent(transform, false);
var src = go.AddComponent<AudioSource>();
src.loop = loop;
src.playOnAwake = false;
src.spatialBlend = 0f;
if (output != null) src.outputAudioMixerGroup = output;
return src;
}
// Создаёт/находит контейнер и пул SFX источников
private void EnsureSfxPool()
{
if (sfxPoolParent == null)
{
var t = transform.Find("SFX Pool");
if (t != null) sfxPoolParent = t;
}
if (sfxPoolParent == null)
{
var go = new GameObject("SFX Pool");
sfxPoolParent = go.transform;
sfxPoolParent.SetParent(transform, false);
}
// Если уже есть дети-аудиоисточники — используем их как пул.
sfxPool.Clear();
var existing = sfxPoolParent.GetComponentsInChildren<AudioSource>(includeInactive: true);
foreach (var src in existing)
{
SetupSfxSource(src);
sfxPool.Add(src);
}
// Добираем до нужного размера
for (int i = sfxPool.Count; i < sfxPoolSize; i++)
{
var src = CreateSfxSource($"SFX_{i:00}");
sfxPool.Add(src);
}
}
private AudioSource CreateSfxSource(string sourceName)
{
var go = new GameObject(sourceName);
go.transform.SetParent(sfxPoolParent, false);
var src = go.AddComponent<AudioSource>();
SetupSfxSource(src);
return src;
}
private void SetupSfxSource(AudioSource src)
{
src.playOnAwake = false;
src.loop = false;
src.spatialBlend = 0f; // по умолчанию 2D
src.volume = sfxDefaultVolume;
if (sfxMixerGroup != null) src.outputAudioMixerGroup = sfxMixerGroup;
}
#endregion
#region Public API
/// <summary>
/// Мгновенно запустить музыку (без фейда).
/// </summary>
public void PlayMusicImmediate(AudioClip clip, bool loop = true, float targetVolume = -1f)
{
if (clip == null) return;
StopCrossfadeAndFade();
musicSource.clip = clip;
musicSource.loop = loop;
musicSource.volume = (targetVolume >= 0f ? targetVolume : musicTargetVolume);
musicSource.Play();
tempMusicSource.Stop();
tempMusicSource.clip = null;
tempMusicSource.volume = 0f;
}
/// <summary>
/// 2) Плавно сменить музыку (кроссфейд A->B).
/// </summary>
public void CrossfadeMusic(AudioClip newClip, float duration = -1f, bool loop = true, float newTargetVolume = -1f)
{
if (newClip == null) return;
float d = (duration > 0f ? duration : defaultFadeDuration);
float toVol = (newTargetVolume >= 0f ? newTargetVolume : musicTargetVolume);
StopCrossfadeAndFade();
crossfadeCoroutine = StartCoroutine(CrossfadeRoutine(newClip, d, loop, toVol));
}
/// <summary>
/// 1) Плавно приостановить музыку (для паузы).
/// pause=true: плавный фейд к 0 и Pause(). pause=false: UnPause() и плавный фейд к musicTargetVolume.
/// </summary>
public void SmoothPauseMusic(bool pause, float duration = -1f)
{
float d = (duration > 0f ? duration : defaultFadeDuration);
if (pause)
{
if (isPausedFaded) return;
storedMusicVolumeBeforePause = musicSource.volume;
StopCrossfadeAndFade();
musicFadeCoroutine = StartCoroutine(FadeOutThenPause(musicSource, d));
isPausedFaded = true;
}
else
{
if (!isPausedFaded) return;
StopCrossfadeAndFade();
musicSource.UnPause();
musicFadeCoroutine = StartCoroutine(Fade(musicSource, from: 0f, to: Mathf.Clamp01(musicTargetVolume), d));
isPausedFaded = false;
}
}
/// <summary>
/// 3) Воспроизвести SFX из пула (2D).
/// </summary>
public void PlaySfx(AudioClip clip, float volume = 1f, float pitch = 1f)
{
if (clip == null) return;
var src = GetFreeSfxSource();
ConfigureAndPlaySfx(src, clip, volume, pitch, spatialBlend: 0f, position: null);
}
/// <summary>
/// 3) Воспроизвести SFX из пула в мире (3D).
/// </summary>
public void PlaySfxAt(AudioClip clip, Vector3 position, float volume = 1f, float pitch = 1f, float spatialBlend = 1f)
{
if (clip == null) return;
var src = GetFreeSfxSource();
ConfigureAndPlaySfx(src, clip, volume, pitch, Mathf.Clamp01(spatialBlend), position);
}
public void PlayRandomSfx(SoundEffect[] sfxAsset)
{
if (sfxAsset.Length == 0) return;
SoundEffect chosenEffect = sfxAsset[Random.Range(0, sfxAsset.Length - 1)];
PlaySfx(chosenEffect.sound, chosenEffect.volume, chosenEffect.pitch);
}
public void PlayRandomSfxAt(SoundEffect[] sfxAsset, Vector3 position)
{
if (sfxAsset.Length == 0) return;
SoundEffect chosenEffect = sfxAsset[Random.Range(0, sfxAsset.Length - 1)];
PlaySfxAt(chosenEffect.sound, position, chosenEffect.volume, chosenEffect.pitch);
}
// ==== Internal helpers ====
private void StopCrossfadeAndFade()
{
if (crossfadeCoroutine != null) { StopCoroutine(crossfadeCoroutine); crossfadeCoroutine = null; }
if (musicFadeCoroutine != null) { StopCoroutine(musicFadeCoroutine); musicFadeCoroutine = null; }
}
private IEnumerator CrossfadeRoutine(AudioClip newClip, float duration, bool loop, float targetVol)
{
// Готовим второй источник
tempMusicSource.clip = newClip;
tempMusicSource.loop = loop;
tempMusicSource.volume = 0f;
tempMusicSource.Play();
float startVolA = musicSource.volume;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime; // не зависим от timescale (музыка при паузе всё равно может фейдиться)
float k = Mathf.Clamp01(t / duration);
// кроссфейд
musicSource.volume = Mathf.Lerp(startVolA, 0f, k);
tempMusicSource.volume = Mathf.Lerp(0f, targetVol, k);
yield return null;
}
// Завершение: стопим A, меняем роли (чтобы основной всегда указывал на текущую музыку)
musicSource.Stop();
musicSource.clip = null;
musicSource.volume = targetVol;
// Меняем ссылки местами
(musicSource, tempMusicSource) = (tempMusicSource, musicSource);
// Убедимся, что запасной источник молчит
tempMusicSource.volume = 0f;
tempMusicSource.Stop();
tempMusicSource.clip = null;
crossfadeCoroutine = null;
}
private IEnumerator FadeOutThenPause(AudioSource src, float duration)
{
float startVol = src.volume;
yield return Fade(src, startVol, 0f, duration);
src.Pause();
musicFadeCoroutine = null;
}
private IEnumerator Fade(AudioSource src, float from, float to, float duration)
{
float t = 0f;
src.volume = from;
if (!src.isPlaying) src.Play();
while (t < duration)
{
t += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(t / duration);
src.volume = Mathf.Lerp(from, to, k);
yield return null;
}
src.volume = to;
}
private AudioSource GetFreeSfxSource()
{
// Сначала ищем свободный
for (int i = 0; i < sfxPool.Count; i++)
{
int idx = (sfxRoundRobinIndex + i) % sfxPool.Count;
if (!sfxPool[idx].isPlaying)
{
sfxRoundRobinIndex = (idx + 1) % sfxPool.Count;
return sfxPool[idx];
}
}
// Если все заняты — берём по round-robin (steal)
var src = sfxPool[sfxRoundRobinIndex];
sfxRoundRobinIndex = (sfxRoundRobinIndex + 1) % sfxPool.Count;
return src;
}
private void ConfigureAndPlaySfx(AudioSource src, AudioClip clip, float volume, float pitch, float spatialBlend, Vector3? position)
{
src.clip = clip;
src.volume = Mathf.Clamp(volume * sfxDefaultVolume, 0f, 1.5f);
src.pitch = Mathf.Clamp(pitch, -3f, 3f);
src.spatialBlend = spatialBlend;
if (position.HasValue)
{
src.transform.position = position.Value;
src.rolloffMode = AudioRolloffMode.Linear;
src.minDistance = 1f;
src.maxDistance = 20f;
}
src.Play();
}
#endregion
}
[Serializable]
public struct SoundEffect
{
public AudioClip sound;
[Min(0f)]public float volume;
public float pitch;
}
}