using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; using Random = UnityEngine.Random; using RR.Common.Audio; using RR.Game2D.Core.Managers; using RR.Common.Settings; 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 = 16; [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; // Текущая громкость для применения к источникам private float currentMusicVolume = 1f; private float currentSfxVolume = 1f; #region Unity Lifetime public void Initialize(AudioMixer mixer = null) { // Singleton init if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); if (mixer != null) { if (musicMixerGroup == null) musicMixerGroup = mixer.FindMatchingGroups("Music")[0]; if (sfxMixerGroup == null) sfxMixerGroup = mixer.FindMatchingGroups("SFX")[0]; } EnsureMusicSources(); EnsureSfxPool(); // Загружаем громкость из настроек при инициализации LoadVolumeFromSettings(); } //private void Awake() //{ //} // Создаёт/находит MusicSource и TempMusicSource private void EnsureMusicSources() { if (musicSource == null) { Transform t = transform.Find("MusicSource"); if (t != null) musicSource = t.GetComponent(); } if (tempMusicSource == null) { Transform 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) { GameObject go = new GameObject(childName); go.transform.SetParent(transform, false); AudioSource 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) { Transform t = transform.Find("SFX Pool"); if (t != null) sfxPoolParent = t; } if (sfxPoolParent == null) { GameObject go = new GameObject("SFX Pool"); sfxPoolParent = go.transform; sfxPoolParent.SetParent(transform, false); } // Если уже есть дети-аудиоисточники — используем их как пул. sfxPool.Clear(); AudioSource[] existing = sfxPoolParent.GetComponentsInChildren(includeInactive: true); foreach (AudioSource 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) { GameObject go = new GameObject(sourceName); go.transform.SetParent(sfxPoolParent, false); AudioSource 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 SetMusicVolume(float newVolume) { currentMusicVolume = Mathf.Clamp01(newVolume); float db = DbFrom01_Log(currentMusicVolume); if (musicMixerGroup?.audioMixer != null) { musicMixerGroup.audioMixer.SetFloat("MusicVolume", db); } } public void SetSfxVolume(float newVolume) { currentSfxVolume = Mathf.Clamp01(newVolume); float db = DbFrom01_Log(currentSfxVolume); if (sfxMixerGroup?.audioMixer != null) { sfxMixerGroup.audioMixer.SetFloat("SFXVolume", db); } } /// /// Мгновенно запустить музыку (без фейда). /// 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(); // Используем сохранённую громкость или текущую из AudioManager float targetVolume = currentMusicVolume; musicFadeCoroutine = StartCoroutine(Fade(musicSource, from: 0f, to: Mathf.Clamp01(targetVolume), 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); } #endregion // ==== 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 static 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) continue; sfxRoundRobinIndex = (idx + 1) % sfxPool.Count; return sfxPool[idx]; } // Если все заняты — берём по round-robin (steal) AudioSource 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(); } private static float DbFrom01_Log(float t, float minDb = -80f, float maxDb = 20f) { t = Mathf.Clamp01(t); float minAmp = Mathf.Pow(10f, minDb / 20f); // 10^(-80/20) = 0.0001 float maxAmp = Mathf.Pow(10f, maxDb / 20f); // 10^(+20/20) = 10 float amp = Mathf.Lerp(minAmp, maxAmp, t); // линейная интерполяция по усилению return 20f * Mathf.Log10(Mathf.Max(1e-6f, amp)); } /// /// Загружает громкость из SettingsManager и применяет её /// private void LoadVolumeFromSettings() { var settings = SettingsManager.Instance; if (settings == null) return; // Загружаем и применяем громкость музыки float musicVol = settings.GetFloat("volume.music"); SetMusicVolume(musicVol); // Загружаем и применяем громкость звуков float sfxVol = settings.GetFloat("volume.sounds"); SetSfxVolume(sfxVol); } } }