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 sfxPool = new List(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(); } if (tempMusicSource == null) { var t = transform.Find("TempMusicSource"); if (t != null) tempMusicSource = t.GetComponent(); } 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(); 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(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(); 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 /// /// Мгновенно запустить музыку (без фейда). /// 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; } /// /// 2) Плавно сменить музыку (кроссфейд A->B). /// 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)); } /// /// 1) Плавно приостановить музыку (для паузы). /// pause=true: плавный фейд к 0 и Pause(). pause=false: UnPause() и плавный фейд к musicTargetVolume. /// 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; } } /// /// 3) Воспроизвести SFX из пула (2D). /// 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); } /// /// 3) Воспроизвести SFX из пула в мире (3D). /// 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; } }