0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MonoBahaviourを継承しないAudioManager

Last updated at Posted at 2024-12-17

はじめに

Unityでサウンドの再生するためのテンプレートコードを作成したため、備忘録として残します。

コード

AudioManager.cs

長いので畳んでます
AudioManager.cs
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

/// <summary> ゲーム内のサウンド管理クラス </summary>
public class AudioManager
{
    private static bool _isUseMainSource = false;
    /// <summary> Audio再生するClipを統括する親オブジェクト </summary>
    private static GameObject _audioObject = default;
    /// <summary> BGM用のSource(基本的に1シーンあたり1BGMなので単一のインスタンス) </summary>
    private static AudioSource _mainBGMSource = default;
    /// <summary> BGM用のSource(BGMのクロスフェードを行う際、1つだとできないため) </summary>
    private static AudioSource _subBGMSource = default;
    /// <summary> SE用のSource(SEはたくさん流れるのでList) </summary>
    private static List<AudioSource> _seSources = default;

    private static AudioHolder _soundHolder = default;

    private static AudioManager _instance = default;

    private readonly Queue<AudioClip> _seQueue = new();

    /// <summary> このクラス内で参照するBGMAudioSource </summary>
    protected AudioSource Source => _isUseMainSource ? _mainBGMSource : _subBGMSource;

    //外部のクラスで参照するBGMAudioSource
    public AudioSource BGMSource => _isUseMainSource ? _mainBGMSource : _subBGMSource;
    public List<AudioSource> SeSources => _seSources;

    public static AudioManager Instance
    {
        get
        {
            if (_instance == null) { Init(); }

            return _instance;
        }
    }

    /// <summary> AudioManagerの初期化処理 </summary>
    private static void Init()
    {
        _audioObject = new GameObject("AudioManager");
        _instance = new();

        var bgm = new GameObject("BGM");
        _mainBGMSource = bgm.AddComponent<AudioSource>();
        bgm.transform.parent = _audioObject.transform;
        _isUseMainSource = true;

        var se = new GameObject("SE");
        _seSources = new() { se.AddComponent<AudioSource>() };
        se.transform.parent = _audioObject.transform;

        _soundHolder = Resources.Load<AudioHolder>("AudioHolder");

        //初期音量設定
        _mainBGMSource.volume = 1f;
        _seSources[0].volume = 1f;

        Object.DontDestroyOnLoad(_audioObject);
    }

    /// <summary> BGM再生 </summary>
    /// <param name="bgm"> どのBGMか </param>
    /// <param name="isLoop"> ループ再生するか(基本的にループする想定のためtrue) </param>
    public void PlayBGM(BGMType bgm, bool isLoop = true)
    {
        var index = -1;
        foreach (var clip in _soundHolder.BGMClips)
        {
            index++;
            if (clip.BGMType == bgm) { break; }
        }
        if (index >= _soundHolder.BGMClips.Length) { Debug.LogError("指定したBGMが見つかりませんでした"); return; }

        Source.Stop();

        Source.loop = isLoop;
        Source.clip = _soundHolder.BGMClips[index].Clip;
        Source.Play();
    }

    /// <summary> SE再生 </summary>
    /// <param name="se"> どのSEか </param>
    public void PlaySE(SEType se)
    {
        var index = -1;
        foreach (var clip in _soundHolder.SEClips)
        {
            index++;
            if (clip.SEType == se) { break; }
        }
        if (index >= _soundHolder.SEClips.Length) { Debug.LogError("指定したSEが見つかりませんでした"); return; }
        //再生するSEを追加
        _seQueue.Enqueue(_soundHolder.SEClips[index].Clip);

        //再生するSEがあれば、最後に追加したSEを再生
        if (_seQueue.Count > 0)
        {
            for (int i = 0; i < _seSources.Count; i++)
            {
                if (!_seSources[i].isPlaying) { _seSources[i].PlayOneShot(_seQueue.Dequeue()); return; }
            }

            var newSource = new GameObject("SE");
            _seSources.Add(newSource.AddComponent<AudioSource>());
            newSource.transform.parent = _audioObject.transform;

            _seSources[^1].PlayOneShot(_seQueue.Dequeue());
        }
    }

    /// <summary> BGMの再生を止める </summary>
    public void StopBGM() => Source.Stop();

    /// <summary> SEの再生を止める </summary>
    public void StopSE()
    {
        foreach (var source in _seSources) { source.Stop(); }
        _seQueue.Clear();
    }

    /// <summary> 指定したシーンのBGMを取得する </summary>
    public AudioClip GetBGMClip(BGMType bgm)
    {
        var index = -1;
        foreach (var clip in _soundHolder.BGMClips)
        {
            index++;
            if (clip.BGMType == bgm) { break; }
        }

        return _soundHolder.BGMClips[index].Clip;
    }

    /// <summary> BGMの再生終了待機 </summary>
    public IEnumerator BGMPlayingWait()
    {
        yield return new WaitUntil(() => !Source.isPlaying);
    }

    public async Task BGMPlaying()
    {
        while (Source.isPlaying) { await Task.Yield(); }
    }

    /// <summary> SEの再生終了待機 </summary>
    public IEnumerator SEPlayingWait()
    {
        foreach (var source in _seSources)
        {
            yield return new WaitUntil(() => !source.isPlaying);
        }
    }

    public async Task SEPlaying()
    {
        foreach (var source in _seSources)
        {
            while (source.isPlaying) { await Task.Yield(); }
        }
    }

    /// <summary> BGM用のAudioSourceを変更する </summary>
    /// <param name="clip"> 新しく再生する音源(指定があれば設定する) </param>
    public void ChangeBGMSource(AudioClip clip = null)
    {
        _isUseMainSource = !_isUseMainSource;

        //2つ目のBGMSourceを初めて使うとき、インスタンスを生成する
        if (!_isUseMainSource && _subBGMSource == null)
        {
            var bgm = new GameObject("SubBGM");
            _subBGMSource = bgm.AddComponent<AudioSource>();
            bgm.transform.parent = _audioObject.transform;
        }
        if (clip != null) { Source.clip = clip; }
    }

    #region 以下Audio系パラメーター設定用の関数
    /// <summary> BGMの音量設定 </summary>
    public void VolumeSettingBGM(float value)
    {
        if (Source == null) { return; }

        Source.volume = value;
    }

    /// <summary> SEの音量設定 </summary>
    public void VolumeSettingSE(float value)
    {
        if (_seSources == null || _seSources.Count <= 0) { return; }

        foreach (var source in _seSources) { source.volume = value; }
    }
    #endregion
}

AudioExtensions.cs

AudioExtensions.cs
using System.Threading.Tasks;
using UnityEngine;

public static class AudioExtensions
{
    private static bool _isFadePlaying = false;

    /// <summary> 音源のクロスフェード </summary>
    /// <param name="source"> 現在のAudioSource </param>
    /// <param name="next"> 次に再生する音源 </param>
    /// <param name="duration"> 実行時間 </param>
    public static async void CrossFade(this AudioSource source, AudioClip next, float duration)
    {
        await CrossFadeAsync(source, next, duration);
    }

    private static async Task CrossFadeAsync(AudioSource source, AudioClip next, float duration)
    {
        var targetVolume = source.volume;
        var endVolume = 0f;

        var audioFadeOut = Task.Run(() => AudioFadeAsync(source, endVolume, duration));
        AudioManager.Instance.ChangeBGMSource(next);
        var audioFadeIn = Task.Run(() => AudioFadeAsync(AudioManager.Instance.BGMSource, targetVolume, duration));

        //複数のTaskを並列実行し、全てが終了するまで待機する
        await Task.WhenAll(audioFadeOut, audioFadeIn);
    }

    /// <summary> 音量を徐々に変更する </summary>
    /// <param name="source"> 対象のAudioSource </param>
    /// <param name="endVolume"> 最終的な音量 </param>
    /// <param name="duration"> 実行時間 </param>
    public static async void DOFade(this AudioSource source, float endVolume, float duration)
    {
        await AudioFadeAsync(source, endVolume, duration);
    }

    private static async Task AudioFadeAsync(AudioSource source, float endVolume, float duration)
    {
        if (_isFadePlaying) { Debug.Log("AudioFade Playing..."); return; }

        _isFadePlaying = true;
        var currentVolume = source.volume;
        if ((int)currentVolume == (int)endVolume) { return; }

        float timer = 0f;
        while (timer <= duration)
        {
            timer += Time.deltaTime;
            source.volume = Mathf.Lerp(currentVolume, endVolume, timer / duration);

            await Task.Yield();
        }

        source.volume = endVolume;
        _isFadePlaying = false;
    }
}

AudioHolder.cs

AudioHolder.cs
using System;
using UnityEngine;

/// <summary> ゲーム内で使用する音をInspectorで設定するためのクラス </summary>
public class AudioHolder : MonoBehaviour
{
    #region Audio Controllers
    [Serializable]
    public class BGMController
    {
        [SerializeField]
        private BGMType _bgmType = default;
        [SerializeField]
        private AudioClip _clip = default;

        public BGMType BGMType => _bgmType;
        public AudioClip Clip => _clip;
    }

    [Serializable]
    public class SEController
    {
        [SerializeField]
        private SEType _seType = default;
        [SerializeField]
        private AudioClip _clip = default;

        public SEType SEType => _seType;
        public AudioClip Clip => _clip;
    }
    #endregion

    [SerializeField]
    private BGMController[] _bgmClips = default;
    [SerializeField]
    private SEController[] _seClips = default;

    public BGMController[] BGMClips => _bgmClips;
    public SEController[] SEClips => _seClips;
}

public enum BGMType
{
    None,
    Title,
    InGame,
}

public enum SEType
{
    None,
    Click,
}

使い方例

「Assets/Resources」内に、以下のようなPrefabを作成します。

スクリーンショット 2024-12-17 172913.png

以下コードサンプルです↓

Sample.cs
using UnityEngine;
using UnityEngine.UI;

public class Sample : MonoBehaviour
{
    [SerializeField]
    private Button _button = default;

    private void Start()
    {
        _button.onClick.AddListener(() =>
        {
           Debug.Log("Button Click"); 
           AudioManager.Instance.PlaySE(SEType.Click);
        });

        AudioManager.Instance.PlayBGM(BGMType.Title);
    }
}

まとめ

呼び出し側的には、割とスッキリした感じになったかなと思います。

拡張メソッド周りは必要に応じて書き足していきます。
ご一読ありがとうございました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?