← Назад к вопросам

Реализовать простой менеджер аудио

1.6 Junior🔥 211 комментариев
#C# и ООП#Unity Core#Оптимизация#Паттерны проектирования

Условие

Реализуйте менеджер аудио для управления звуками и музыкой в игре.

Требования

  1. Singleton паттерн для доступа из любого места
  2. Раздельное управление громкостью музыки и звуковых эффектов
  3. Методы:
    • PlayMusic(AudioClip clip) - играть фоновую музыку с плавным переходом
    • PlaySFX(AudioClip clip) - воспроизвести звуковой эффект
    • PlaySFXAtPosition(AudioClip clip, Vector3 position) - 3D звук
    • SetMusicVolume(float volume)
    • SetSFXVolume(float volume)
    • Mute() / Unmute()
  4. Сохранение настроек громкости в PlayerPrefs

Бонус

  • Пул AudioSource для звуковых эффектов
  • Плавный кроссфейд между треками

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Архитектура AudioManager

Хороший аудио менеджер должен быть простым, гибким и эффективным. Я реализую систему с поддержкой пула AudioSource для звуковых эффектов, раздельным управлением музыкой и SFX, а также плавными кроссфейдами между треками.

Основной AudioManager

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class AudioManager : Singleton<AudioManager>
{
    [Header("Audio Sources")]
    [SerializeField] private AudioSource musicSource;
    [SerializeField] private Transform sfxPoolParent;
    [SerializeField] private int sfxPoolSize = 10;
    
    [Header("Volume Settings")]
    [SerializeField] private float masterVolume = 1f;
    [SerializeField] private float musicVolume = 0.7f;
    [SerializeField] private float sfxVolume = 0.8f;
    [SerializeField] private float musicFadeDuration = 1f; // Кроссфейд между треками
    
    [Header("Mute Settings")]
    [SerializeField] private bool isMuted = false;
    
    // Пул звуковых эффектов
    private Queue<AudioSource> sfxPool;
    private AudioSource currentMusicSource;
    private Coroutine musicFadeCoroutine;
    
    // Константы для PlayerPrefs
    private const string MUSIC_VOLUME_KEY = "AudioManager_MusicVolume";
    private const string SFX_VOLUME_KEY = "AudioManager_SFXVolume";
    private const string MASTER_VOLUME_KEY = "AudioManager_MasterVolume";
    private const string MUTE_KEY = "AudioManager_IsMuted";
    
    protected override void Awake()
    {
        base.Awake();
        
        // Инициализируем пул
        InitializeSFXPool();
        
        // Загружаем сохранённые настройки
        LoadVolumeSettings();
    }
    
    void Start()
    {
        // Создаём AudioSource для музыки если его нет
        if (musicSource == null)
        {
            GameObject musicObj = new GameObject("MusicSource");
            musicObj.transform.SetParent(transform);
            musicSource = musicObj.AddComponent<AudioSource>();
            musicSource.loop = true;
            currentMusicSource = musicSource;
        }
    }
    
    /// <summary>Инициализируем пул AudioSource для SFX</summary>
    private void InitializeSFXPool()
    {
        sfxPool = new Queue<AudioSource>(sfxPoolSize);
        
        // Создаём парент для организации в иерархии
        if (sfxPoolParent == null)
        {
            GameObject poolParent = new GameObject("SFXPool");
            poolParent.transform.SetParent(transform);
            sfxPoolParent = poolParent.transform;
        }
        
        // Создаём и добавляем AudioSource в пул
        for (int i = 0; i < sfxPoolSize; i++)
        {
            CreateSFXSource();
        }
    }
    
    /// <summary>Создать новый AudioSource для SFX</summary>
    private void CreateSFXSource()
    {
        GameObject sfxObj = new GameObject("SFX_Source");
        sfxObj.transform.SetParent(sfxPoolParent);
        
        AudioSource audioSource = sfxObj.AddComponent<AudioSource>();
        audioSource.playOnAwake = false;
        audioSource.loop = false;
        
        sfxPool.Enqueue(audioSource);
    }
    
    /// <summary>Получить AudioSource из пула (или создать новый если пул пуст)</summary>
    private AudioSource GetSFXSource()
    {
        AudioSource source;
        
        if (sfxPool.Count > 0)
        {
            source = sfxPool.Dequeue();
        }
        else
        {
            // Если пул пуст, создаём новый источник
            CreateSFXSource();
            source = sfxPool.Dequeue();
            Debug.LogWarning("SFX пул переполнен, создан дополнительный AudioSource");
        }
        
        return source;
    }
    
    /// <summary>Вернуть AudioSource в пул</summary>
    private void ReturnSFXSource(AudioSource source)
    {
        if (sfxPool.Count < sfxPoolSize)
        {
            source.clip = null;
            source.Stop();
            sfxPool.Enqueue(source);
        }
        else
        {
            Destroy(source.gameObject);
        }
    }
    
    /// <summary>Воспроизвести музыку с кроссфейдом</summary>
    public void PlayMusic(AudioClip clip)
    {
        if (clip == null)
        {
            Debug.LogError("Попытка проиграть null AudioClip как музыку");
            return;
        }
        
        // Останавливаем текущий кроссфейд если он есть
        if (musicFadeCoroutine != null)
        {
            StopCoroutine(musicFadeCoroutine);
        }
        
        // Начинаем кроссфейд из текущей музыки в новую
        musicFadeCoroutine = StartCoroutine(CrossfadeMusic(clip));
    }
    
    /// <summary>Кроссфейд между музыкальными треками</summary>
    private IEnumerator CrossfadeMusic(AudioClip newClip)
    {
        float fadeOutTime = musicFadeDuration * 0.5f;
        float fadeInTime = musicFadeDuration * 0.5f;
        
        // Fade out текущей музыки
        float elapsedTime = 0f;
        while (elapsedTime < fadeOutTime && musicSource.volume > 0)
        {
            elapsedTime += Time.deltaTime;
            musicSource.volume = Mathf.Lerp(musicVolume, 0, elapsedTime / fadeOutTime);
            yield return null;
        }
        
        musicSource.Stop();
        musicSource.clip = newClip;
        musicSource.volume = 0;
        musicSource.Play();
        
        // Fade in новой музыки
        elapsedTime = 0f;
        while (elapsedTime < fadeInTime && musicSource.volume < musicVolume)
        {
            elapsedTime += Time.deltaTime;
            musicSource.volume = Mathf.Lerp(0, musicVolume, elapsedTime / fadeInTime);
            yield return null;
        }
        
        musicSource.volume = musicVolume;
        Debug.Log($"Музыка изменена на {newClip.name}");
    }
    
    /// <summary>Воспроизвести звуковой эффект</summary>
    public void PlaySFX(AudioClip clip, float volumeOverride = -1f)
    {
        if (clip == null)
        {
            Debug.LogError("Попытка проиграть null AudioClip как SFX");
            return;
        }
        
        AudioSource source = GetSFXSource();
        source.clip = clip;
        source.volume = volumeOverride >= 0 ? volumeOverride : sfxVolume;
        source.spatialBlend = 0f; // 2D звук
        source.Play();
        
        // Возвращаем источник в пул после окончания звука
        StartCoroutine(ReturnSourceToPool(source, clip.length));
    }
    
    /// <summary>Воспроизвести 3D звук в определённой позиции</summary>
    public void PlaySFXAtPosition(AudioClip clip, Vector3 position, float volumeOverride = -1f)
    {
        if (clip == null)
        {
            Debug.LogError("Попытка проиграть null AudioClip как 3D SFX");
            return;
        }
        
        AudioSource source = GetSFXSource();
        source.clip = clip;
        source.volume = volumeOverride >= 0 ? volumeOverride : sfxVolume;
        source.spatialBlend = 1f; // 3D звук
        source.transform.position = position;
        source.Play();
        
        // Возвращаем источник в пул после окончания звука
        StartCoroutine(ReturnSourceToPool(source, clip.length));
    }
    
    /// <summary>Возвращение источника в пул после завершения воспроизведения</summary>
    private IEnumerator ReturnSourceToPool(AudioSource source, float duration)
    {
        yield return new WaitForSeconds(duration);
        ReturnSFXSource(source);
    }
    
    /// <summary>Остановить музыку</summary>
    public void StopMusic()
    {
        if (musicSource != null)
        {
            musicSource.Stop();
        }
    }
    
    /// <summary>Паузировать музыку</summary>
    public void PauseMusic()
    {
        if (musicSource != null && musicSource.isPlaying)
        {
            musicSource.Pause();
        }
    }
    
    /// <summary>Возобновить музыку</summary>
    public void ResumeMusic()
    {
        if (musicSource != null && !musicSource.isPlaying)
        {
            musicSource.UnPause();
        }
    }
    
    /// <summary>Установить громкость музыки (0-1)</summary>
    public void SetMusicVolume(float volume)
    {
        musicVolume = Mathf.Clamp01(volume);
        if (musicSource != null)
        {
            musicSource.volume = musicVolume * masterVolume * (isMuted ? 0 : 1);
        }
        SaveVolumeSettings();
    }
    
    /// <summary>Установить громкость SFX (0-1)</summary>
    public void SetSFXVolume(float volume)
    {
        sfxVolume = Mathf.Clamp01(volume);
        SaveVolumeSettings();
    }
    
    /// <summary>Установить главную громкость (0-1)</summary>
    public void SetMasterVolume(float volume)
    {
        masterVolume = Mathf.Clamp01(volume);
        UpdateAllVolumes();
        SaveVolumeSettings();
    }
    
    /// <summary>Получить текущую громкость музыки</summary>
    public float GetMusicVolume() => musicVolume;
    
    /// <summary>Получить текущую громкость SFX</summary>
    public float GetSFXVolume() => sfxVolume;
    
    /// <summary>Получить главную громкость</summary>
    public float GetMasterVolume() => masterVolume;
    
    /// <summary>Отключить звук</summary>
    public void Mute()
    {
        isMuted = true;
        UpdateAllVolumes();
        SaveVolumeSettings();
        Debug.Log("Звук отключён");
    }
    
    /// <summary>Включить звук</summary>
    public void Unmute()
    {
        isMuted = false;
        UpdateAllVolumes();
        SaveVolumeSettings();
        Debug.Log("Звук включён");
    }
    
    /// <summary>Переключить звук</summary>
    public void ToggleMute()
    {
        if (isMuted)
        {
            Unmute();
        }
        else
        {
            Mute();
        }
    }
    
    /// <summary>Проверить отключён ли звук</summary>
    public bool IsMuted() => isMuted;
    
    /// <summary>Обновить громкость всех источников</summary>
    private void UpdateAllVolumes()
    {
        float effectiveVolume = isMuted ? 0 : 1;
        
        if (musicSource != null)
        {
            musicSource.volume = musicVolume * masterVolume * effectiveVolume;
        }
    }
    
    /// <summary>Сохранить настройки громкости</summary>
    private void SaveVolumeSettings()
    {
        PlayerPrefs.SetFloat(MASTER_VOLUME_KEY, masterVolume);
        PlayerPrefs.SetFloat(MUSIC_VOLUME_KEY, musicVolume);
        PlayerPrefs.SetFloat(SFX_VOLUME_KEY, sfxVolume);
        PlayerPrefs.SetInt(MUTE_KEY, isMuted ? 1 : 0);
        PlayerPrefs.Save();
    }
    
    /// <summary>Загрузить сохранённые настройки громкости</summary>
    private void LoadVolumeSettings()
    {
        masterVolume = PlayerPrefs.GetFloat(MASTER_VOLUME_KEY, 1f);
        musicVolume = PlayerPrefs.GetFloat(MUSIC_VOLUME_KEY, 0.7f);
        sfxVolume = PlayerPrefs.GetFloat(SFX_VOLUME_KEY, 0.8f);
        isMuted = PlayerPrefs.GetInt(MUTE_KEY, 0) == 1;
        
        UpdateAllVolumes();
    }
    
    /// <summary>Получить информацию о пуле</summary>
    public string GetPoolStats()
    {
        return $"SFX Pool: {sfxPool.Count}/{sfxPoolSize} доступно";
    }
}

Интеграция с UI (громкость)

public class AudioSettingsUI : MonoBehaviour
{
    [SerializeField] private Slider musicVolumeSlider;
    [SerializeField] private Slider sfxVolumeSlider;
    [SerializeField] private Slider masterVolumeSlider;
    [SerializeField] private Toggle muteToggle;
    [SerializeField] private Text volumeLabel;
    
    private AudioManager audioManager;
    
    void Start()
    {
        audioManager = AudioManager.Instance;
        
        // Инициализируем слайдеры
        musicVolumeSlider.value = audioManager.GetMusicVolume();
        sfxVolumeSlider.value = audioManager.GetSFXVolume();
        masterVolumeSlider.value = audioManager.GetMasterVolume();
        muteToggle.isOn = audioManager.IsMuted();
        
        // Подписываемся на события
        musicVolumeSlider.onValueChanged.AddListener(audioManager.SetMusicVolume);
        sfxVolumeSlider.onValueChanged.AddListener(audioManager.SetSFXVolume);
        masterVolumeSlider.onValueChanged.AddListener(audioManager.SetMasterVolume);
        muteToggle.onValueChanged.AddListener(_ => audioManager.ToggleMute());
    }
}

Примеры использования

Воспроизведение музыки:

AudioManager.Instance.PlayMusic(battleMusicClip);

Воспроизведение звука:

AudioManager.Instance.PlaySFX(swordSwingClip);

Воспроизведение 3D звука:

AudioManager.Instance.PlaySFXAtPosition(explosionClip, explosionPosition);

Управление громкостью:

AudioManager.Instance.SetMasterVolume(0.5f);
AudioManager.Instance.SetMusicVolume(0.8f);

Отключение звука:

AudioManager.Instance.Mute();

Ключевые особенности

1. Пул AudioSource — переиспользуем источники вместо создания новых каждый раз.

2. Кроссфейд — плавный переход между музыкальными треками без пауз.

3. Раздельное управление — независимые громкости для музыки и SFX.

4. Сохранение настроек — громкость сохраняется в PlayerPrefs.

5. 2D и 3D звуки — поддержка как обычных так и пространственных звуков.

6. Оптимизация — Coroutine для автоматического возврата источников в пул.

Эта система обеспечивает гибкость, производительность и удобство в управлении аудио.

Реализовать простой менеджер аудио | PrepBro