-Add LevelSpecificPlayerSFX prefabs for levels 1-1 and 1-2 -Configure checkpoints to activate sound effects, including pitch, volume, and audio editing -Rename the checkpoint effect object to Checkpoint VFX -Update the audio manager, the default value of the music target volume is 0.2 -Add SoundEffect structure to support sound parameter configuration -Optimize audio source creation and crossfade logic -Remove the old music control components and use the new audio management method
385 lines
14 KiB
C#
385 lines
14 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using Random = UnityEngine.Random;
|
|
|
|
using RR.Common.Audio;
|
|
|
|
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 = 0.2f;
|
|
|
|
[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
|
|
}
|
|
} |