Unityで使えるサウンドマネージャー

# 最初に
Unityのサウンドマネージャーです
想定しているのはボードゲームやパズルゲームなどです、３Dアクションなどの使う音源が多い場合は一括で読み込んで管理はしないほうがいいと思います
しかし、複数のシーンを跨いでBGMを鳴らすのならばこのサウンドマネージャーのBGM部分だけを切り出して使うのがいいと思います

また、サウンドごとの音量調整をしたい場合はこれをそのままオブジェクトにAddしてプレハブを作りそれを生成するといいと思います

音の臨場感を大切にする場合はAudioMixerで調整した方がいいので向いてません


三箇所切り出して説明を入れてますが、コード全文の方が見やすいのでページ最下部に載せています

##環境
Unity2017.3.1.p4
.NET4.6

Unityのバージョンは特に気にすることはないと思います
Task,async,awaitや文字列補間など使ってないので、.NET3.5でも問題ない気がします

## 1.リソースから読み取り
BGMとSEをResoucesから読み込んで管理します
SEは複数同時に鳴らせるようにAudioSourceをSE_SOURCE_NUMで定義しました
BGMLabelとSELabelがありますが、自分は二つ以上のクラスで使う列挙型はまとめてCommon.csで定義しています

<font color="Orange">あとこの二つのラベルはAudio/BGMとAudio/SEにそれぞれ置かれている音源のファイル名と全く同じくして下さい</font>
再生する時にLabel.ToString()で辞書型から探しています
ここの実装は悩みましたが、Inspectorで割り当てずに読み込む方針で作っているのでこれがいいと思います

```c#

private const string BGM_PATH = "Audio/BGM";
private const string SE_PATH = "Audio/SE";
private const int BGM_SOURCE_NUM = 1;
private const int SE_SOURCE_NUM = 5;

// BGMは一つづつ鳴るが、SEは複数同時に鳴ることがある
private AudioSource bgmSource;
private List<AudioSource> seSourceList;
private Dictionary<string, AudioClip> seClipDic;
private Dictionary<string, AudioClip> bgmClipDic;


for (int i = 0; i < SE_SOURCE_NUM + BGM_SOURCE_NUM; i++)
{
    gameObject.AddComponent<AudioSource>();
}

// LinQを使い、BGMはループさせ、ボリュームをそれぞれ代入しました。
IEnumerable<AudioSource> audioSources = GetComponents<AudioSource>().Select(a => { a.playOnAwake = false; a.volume = BGM_VOLUME; a.loop = true; return a; });
bgmSource = audioSources.First();
seSourceList = audioSources.Skip(BGM_SOURCE_NUM).ToList();
seSourceList.ForEach(a => { a.volume = SE_VOLUME; a.loop = false; });

bgmClipDic = (Resources.LoadAll(BGM_PATH) as Object[]).ToDictionary(bgm => bgm.name, bgm => (AudioClip)bgm);
seClipDic = (Resources.LoadAll(SE_PATH) as Object[]).ToDictionary(se => se.name, se => (AudioClip)se);

public enum BGMLabel
{
    None,
    Home,
    Game,
    Library
}

public enum SELabel
{
    Start,
    PlayGame,
    TapButton,
    Efect,
    Win,
    Lose,
    ItemGet
}
```

## 2.BGMはフェードアウトで遷移
BGMはフェードアウトで遷移できるようしました

```c#
private const float BGM_VOLUME = 0.5f;
private const float FADE_OUT_SECONDO = 0.5f;

private bool isFadeOut = false;
private float fadeDeltaTime = 0f;
private BGMLabel nextBGM = BGMLabel.None;
private AudioSource bgmSource;

private void Update()
{
    if (isFadeOut)
    {
        fadeDeltaTime += Time.deltaTime;
        bgmSource.volume = (1.0f - fadeDeltaTime / FADE_OUT_SECONDO) * BGM_VOLUME;

        if (fadeDeltaTime >= FADE_OUT_SECONDO)
        {
            isFadeOut = false;
            bgmSource.Stop();
        }
    }
    else if (nextBGM != BGMLabel.None)
    {
        bgmSource.volume = BGM_VOLUME;
        PlayBGM(nextBGM);
    }
}
```
## 3.デバックもやりやすいシングルトン
スタート画面のシーンに置いたとしても、デバック時に途中のシーンを再生したらエラーになります
その対策として、必要になった時に初めてGameObjectを生成するようにしました

```c#
private static SoundManager singletonInstance = null;

public static SoundManager SingletonInstance
{
    get
    {
        if (!singletonInstance)
        {
            GameObject obj = new GameObject(SOUND_OBJECT_NAME);
            singletonInstance = obj.AddComponent<SoundManager>();
            DontDestroyOnLoad(obj);
        }
        return singletonInstance;
    }
}
```

## コード全文

```c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;


public class SoundManager : MonoBehaviour
{
    private const string BGM_PATH = "Audio/BGM";
    private const string SE_PATH = "Audio/SE";
    private const string SOUND_OBJECT_NAME = "SoundManager";
    private const int BGM_SOURCE_NUM = 1;
    private const int SE_SOURCE_NUM = 5;
    private const float FADE_OUT_SECONDO = 0.5f;
    private const float BGM_VOLUME = 0.5f;
    private const float SE_VOLUME = 0.3f;

    private bool isFadeOut = false;
    private float fadeDeltaTime = 0f;
    private int nextSESourceNum = 0;
    private BGMLabel currentBGM = BGMLabel.None;
    private BGMLabel nextBGM = BGMLabel.None;

    // BGMは一つづつ鳴るが、SEは複数同時に鳴ることがある
    private AudioSource bgmSource;
    private List<AudioSource> seSourceList;
    private Dictionary<string, AudioClip> seClipDic;
    private Dictionary<string, AudioClip> bgmClipDic;
    private static SoundManager singletonInstance = null;


    public static SoundManager SingletonInstance
    {
        get
        {
            if (!singletonInstance)
            {
                GameObject obj = new GameObject(SOUND_OBJECT_NAME);
                singletonInstance = obj.AddComponent<SoundManager>();
                DontDestroyOnLoad(obj);
            }
            return singletonInstance;
        }
    }


    private void Awake()
    {
        for (int i = 0; i < SE_SOURCE_NUM + BGM_SOURCE_NUM; i++)
        {
            gameObject.AddComponent<AudioSource>();
        }

        IEnumerable<AudioSource> audioSources = GetComponents<AudioSource>().Select(a => { a.playOnAwake = false; a.volume = BGM_VOLUME; a.loop = true; return a; });
        bgmSource = audioSources.First();
        seSourceList = audioSources.Skip(BGM_SOURCE_NUM).ToList();
        seSourceList.ForEach(a => { a.volume = SE_VOLUME; a.loop = false; });

        bgmClipDic = (Resources.LoadAll(BGM_PATH) as Object[]).ToDictionary(bgm => bgm.name, bgm => (AudioClip)bgm);
        seClipDic = (Resources.LoadAll(SE_PATH) as Object[]).ToDictionary(se => se.name, se => (AudioClip)se);
    }


    /// <summary>
    /// 指定したファイル名のSEを流す。第二引数のdelayに指定した時間だけ再生までの間隔を空ける
    /// </summary>
    /// /// <param name="seLabel"></param>
    /// /// <param name="delay"></param>
    public void PlaySE(SELabel seLabel, float delay = 0.0f) => StartCoroutine(DelayPlaySE(seLabel, delay));

    private IEnumerator DelayPlaySE(SELabel seLabel, float delay)
    {
        yield return new WaitForSeconds(delay);
        AudioSource se = seSourceList[nextSESourceNum];
        se.PlayOneShot(seClipDic[seLabel.ToString()]);
        nextSESourceNum = (++nextSESourceNum < SE_SOURCE_NUM) ? nextSESourceNum : 0;
    }


    /// <summary>
    /// 指定したBGMを流す。すでに流れている場合はNextに予約し、流れているBGMをフェードアウトさせる
    /// </summary>
    /// <param name="bgmLabel"></param>
    public void PlayBGM(BGMLabel bgmLabel)
    { if (!bgmSource.isPlaying)
        {
            currentBGM = bgmLabel;
            nextBGM = BGMLabel.None;
            if (bgmClipDic.ContainsKey(bgmLabel.ToString()))
            {
                bgmSource.clip = bgmClipDic[bgmLabel.ToString()];
            }
            else
            {
                Debug.LogError($"bgmClipDicに{bgmLabel.ToString()}というKeyはありません");
            }
            bgmSource.Play();
        }
        else if (currentBGM != bgmLabel)
        {
            isFadeOut = true;
            nextBGM = bgmLabel;
            fadeDeltaTime = 0f;
        }
    }


    /// <summary>
    /// BGMを止める
    /// </summary>
    public void StopSound()
    {
        bgmSource.Stop();
        seSourceList.ForEach(a => { a.Stop(); });
    }


    private void Update()
    {
        if (isFadeOut)
        {
            fadeDeltaTime += Time.deltaTime;
            bgmSource.volume = (1.0f - fadeDeltaTime / FADE_OUT_SECONDO) * BGM_VOLUME;

            if (fadeDeltaTime >= FADE_OUT_SECONDO)
            {
                isFadeOut = false;
                bgmSource.Stop();
            }
        }
        else if (nextBGM != BGMLabel.None)
        {
            bgmSource.volume = BGM_VOLUME;
            PlayBGM(nextBGM);
        }
    }
}


public enum BGMLabel
{
    None,
    Home,
    Game,
    Library
}

public enum SELabel
{
    Start,
    PlayGame,
    TapButton,
    Spell,
    Win,
    Lose,
    ItemGet
}



```
#最後に
改善点があったり、そもそもこの設計でやることはどんな状況でもありえないといった場合はコメントして下さると嬉しいです。