Unityで便利なBGM&SE再生マネージャーを作る【1行呼び出し＆多重再生対応】

# はじめに
Unityで開発をしていると，シーンのどこからでも簡単に音を鳴らしたくなる場面が頻繁にあります．そこで以下の記事を参考に機能を追加して，「どのスクリプトからでも1行でSEを鳴らせるSoundManager」を実装しました．

参考記事 :
[【Unity入門】汎用サウンドマネージャー(Sound Manager)の作り方 前編](https://feynman.co.jp/unityforest/unity-introduction/making-sound-manager/)

本記事のSoundManagerは，以下のような開発上のニーズに応えることを目的としています：

- どのスクリプトからでも1行でSEを鳴らせるようにする
- SEの重複再生を可能にする
- GitHubなどでリポジトリを公開する際に，商用音源の再配布リスクを避ける
- 共同開発で毎回音源をInspectorにアタッチする手間を無くしたい

つまり，利便性・再配布リスクの回避・チーム開発の快適さを両立する仕組みを目指しました．

# 使用するコード
<details>
<summary>SoundManager.csの全コード（クリックで展開）</summary>

```csharp:SoundManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Audio;


public class SoundManager : MonoBehaviour
{
    [SerializeField] AudioSource bgmAudioSource; // BGM用のAudioSource
    [SerializeField] AudioSource seAudioSource;  // SE（効果音）用のAudioSource（未使用）
    [SerializeField] AudioMixerGroup mixerGroup; // 出力先のAudioMixer

    [SerializeField] List<BGMSoundData> bgmSoundDatas; // BGMのデータ一覧
    [SerializeField] List<SESoundData> seSoundDatas;   // SEのデータ一覧

    public float masterVolume = 1;     // 全体の音量
    public float bgmMasterVolume = 1;  // BGMのマスター音量
    public float seMasterVolume = 1;   // SEのマスター音量

    int maxSeCount = 10; // 同時に鳴らせるSEの最大数
    List<GameObject> seGameObjects = new(); // 再生中のSE用GameObjectのリスト

    public static SoundManager Instance { get; private set; } // シングルトンインスタンス
    private string currentBGM = null; // 現在再生中のBGM名

    /// <summary>
    /// シングルトン処理
    /// </summary>
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);  // シーンをまたいでも破棄されない
            LoadAllAudioClips();            // BGM・SEのAudioClipを読み込む
        }
        else
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// ResourcesフォルダからBGM・SEを自動で読み込む
    /// </summary>
    private void LoadAllAudioClips()
    {
        foreach (var data in bgmSoundDatas)
        {
            if (data.audioClip == null)
            {
                // Resources/BGM/ にあるAudioClipを読み込む
                data.audioClip = Resources.Load<AudioClip>("BGM/" + data.bgmName);
                if (data.audioClip == null)
                {
                    Debug.LogWarning($"BGM '{data.bgmName}' のAudioClipがResources/BGMから見つかりませんでした．");
                }
            }
        }

        foreach (var data in seSoundDatas)
        {
            if (data.audioClip == null)
            {
                // Resources/SE/ にあるAudioClipを読み込む
                data.audioClip = Resources.Load<AudioClip>("SE/" + data.seName);
                if (data.audioClip == null)
                {
                    Debug.LogWarning($"SE '{data.seName}' のAudioClipがResources/SEから見つかりませんでした．");
                }
            }
        }
    }

    /// <summary>
    /// BGMを再生する関数
    /// </summary>
    /// <param name="bgmName"></param>
    public void PlayBGM(string bgmName)
    {
        // すでに同じBGMが再生中なら処理しない
        if (currentBGM != null && currentBGM.ToString() == bgmName) return;

        // 指定名のBGMデータを探す
        BGMSoundData data = bgmSoundDatas.Find(data => data.bgmName == bgmName);
        if (data == null)
        {
            Debug.LogWarning($"BGM '{bgmName}' が見つかりませんでした．");
            return;
        }

        // AudioClipと音量を設定して再生
        bgmAudioSource.clip = data.audioClip;
        bgmAudioSource.volume = data.volume * bgmMasterVolume * masterVolume;
        bgmAudioSource.Play();

        currentBGM = bgmName;  // 現在のBGM名を記録
    }

    /// <summary>
    /// 効果音を再生する関数
    /// </summary>
    /// <param name="seName"></param>
    public void PlaySE(string seName)
    {
        // nullになったGameObjectを削除（破棄されたSEの後処理）
        seGameObjects.RemoveAll(x => x == null);    

        // 同時に鳴らせるSEの上限チェック
        if (seGameObjects.Count > maxSeCount) return;

        // SEデータを検索
        SESoundData data = seSoundDatas.Find(d => d.seName == seName);
        if (data == null)
        {
            Debug.LogWarning($"SE '{seName}' が見つかりませんでした．");
            return;
        }

        // 一時的なGameObjectを生成し，そこにAudioSourceをアタッチ
        GameObject tempGO = new GameObject("TempSE_" + data.audioClip.name);
        AudioSource tempSource = tempGO.AddComponent<AudioSource>();
        tempSource.outputAudioMixerGroup = mixerGroup;

        // SE再生用GameObjectを管理リストに追加し，破棄されないように設定
        seGameObjects.Add(tempGO);
        DontDestroyOnLoad(tempGO);

        // SEのAudioClipと音量を設定して再生
        tempSource.clip = data.audioClip;
        tempSource.volume = data.volume * seMasterVolume * masterVolume;
        tempSource.Play();

        // 再生終了後に自動でGameObjectを破棄
        Destroy(tempGO, data.audioClip.length);
    }

    void OnEnable()
    {
        // シーンがロードされたときに呼び出されるイベントに登録
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    void OnDisable()
    {
        // イベント登録を解除
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    /// <summary>
    /// シーン切り替え時に実行される関数
    /// </summary>
    /// <param name="scene"></param>
    /// <param name="mode"></param>
    void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        PlayBgmBySceneName();
    }

    void Start()
    {
        PlayBgmBySceneName();   // 起動時にもBGMを再生（Awake後）
    }

    /// <summary>
    /// シーン名に応じたBGMを再生する
    /// </summary>
    void PlayBgmBySceneName()
    {
        switch (SceneManager.GetActiveScene().name)
        {
            case "TitleScene":
            case "StageSelectScene":
                SoundManager.Instance.PlayBGM("Main");
                break;

            case "Stage1":
                SoundManager.Instance.PlayBGM("Stage1"); break;

            case "Stage2":
                SoundManager.Instance.PlayBGM("Stage2"); break;

            case "Stage3":
                SoundManager.Instance.PlayBGM("Stage3"); break;

            case "Stage4":
                SoundManager.Instance.PlayBGM("Stage4"); break;

            case "Stage5":
                SoundManager.Instance.PlayBGM("Stage5"); break;
        }
    }
}

[System.Serializable]
public class BGMSoundData
{
    public string bgmName;          // BGMの識別名
    public AudioClip audioClip;     // BGMの実データ
    [Range(0, 1)]
    public float volume = 1;        // 個別音量（0〜1）
}

[System.Serializable]
public class SESoundData
{
    public string seName;           // SEの識別名
    public AudioClip audioClip;     // SEの実データ
    [Range(0, 1)]
    public float volume = 1;        // 個別音量（0〜1）
}
```

</details>


# 手順
この`SoundManager`を利用するための手順は以下の通りです：

### 1．SoundManagerオブジェクトの作成
ゲームシーン上に空のGameObjectを作成し，名前を `SoundManager` にします．  
このGameObjectに，本記事で紹介している `SoundManager.cs` をアタッチします．

### 2．Audio Sourceの準備

`SoundManager` オブジェクトのコンポーネントに **Audio Sourceを2つ追加** します．  
- 1つは **BGM再生用**
- もう1つは **SE再生用**

それぞれのAudio Sourceを，`SoundManager`スクリプトの `Bgm Audio Source` と `Se Audio Source` のフィールドにドラッグ＆ドロップで登録します．

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3964576/235ae3c0-e982-429c-a078-ddcbb5bc5f5c.png" width="200">

### 3．Audio Mixerを使用している場合（任意）

プロジェクトで **Audio Mixer** による音量管理を行っている場合は，  
`SoundManager`スクリプトに **SE再生用のAudio Sourceに** 対応する **Audio Mixer Group** をアサインします．

> ※SEについては，必ずAudio Mixer Groupを登録しておく必要があります．
なぜなら，SE再生時にはスクリプト内で一時的な GameObject を生成し，そこに AudioSource をアタッチしているためです．このとき，Audio Mixer Group が設定されていないと，音量などのMixerの調整値が適用されません． 

Audio Mixerを使ったスライダーで音量調節する機能は
[Audio Mixerとスライダーを使った音量調節の方法 - calm_otterさんのNote](https://note.com/calm_otter351/n/need65fbb2810#318447ec-4666-41b6-8387-8b1e0b8db59f)
を使用しております．

### 4．使用するBGM&SEを登録
① SoundManager オブジェクトのインスペクタで，Bgm Sound Datas と Se Sound Datas に，使用したい音源と同じ名前でデータを登録します．このとき**拡張子の記述は不要**です．

② Assets フォルダ内に Resources フォルダを作成します．

③ その中に，BGM 用・SE 用のフォルダを作成します．
さらに`.gitignore`に
```
Assets/Resources/SE/
Assets/Resources/BGM/
```
を追加しておきます．（リポジトリを公開しない場合は音源の再配布にならないので`.gitignore`をいじる必要はありません）

④ 各フォルダに使用したい音源ファイルを配置します：
- BGM フォルダ → BGM音源（例：MainTheme.mp3 など）
- SE フォルダ → SE音源（例：Click.wav, Explosion.wav など）

```markdown:ディレクトリ構成
Assets/
└── Resources/
    ├── SE/
    │   └── Back.wav
    └── BGM/
        └── stage1.mp3
```

:::note warn
音源ファイル名は，SoundManager に登録した名前と完全に一致させる必要があります．ファイル名が異なると正しく読み込まれず，音が鳴りません．
:::

ここまでの設定が完了すれば，以下のように **任意のスクリプトから1行でSEやBGMを再生** できるようになります：

```csharp
SoundManager.Instance.PlaySE("BGM名");
SoundManager.Instance.PlayBGM("SE名");
```

# コードの解説

## AudioClipの読み込み処理

```csharp
/// <summary>
/// ResourcesフォルダからBGM・SEを自動で読み込む
/// </summary>
private void LoadAllAudioClips()
{
    foreach (var data in bgmSoundDatas)
    {
        if (data.audioClip == null)
        {
            // Resources/BGM/ にあるAudioClipを読み込む
            data.audioClip = Resources.Load<AudioClip>("BGM/" + data.bgmName);
            if (data.audioClip == null)
            {
                Debug.LogWarning($"BGM '{data.bgmName}' のAudioClipがResources/BGMから見つかりませんでした．");
            }
        }
    }

    foreach (var data in seSoundDatas)
    {
        if (data.audioClip == null)
        {
            // Resources/SE/ にあるAudioClipを読み込む
            data.audioClip = Resources.Load<AudioClip>("SE/" + data.seName);
            if (data.audioClip == null)
            {
                Debug.LogWarning($"SE '{data.seName}' のAudioClipがResources/SEから見つかりませんでした．");
            }
        }
    }
}
```

この関数は，SoundManagerが保持するBGMとSEデータ一覧（`bgmSoundDatas`，`seSoundDatas`）に対して，自動的にBGMフォルダとSEフォルダから該当する音声ファイルを読み込んで設定する処理です．

読み込む際は，各データに登録されたBGM名（`bgmName`，`seName`）をファイル名として使用し，`Resources.Load<AudioClip>("BGM/◯◯")`の形式で取得を行います．

このように，BGM用の音声ファイルを自動で読み込み，開発時の手動設定の手間を減らし，設定漏れやミスを防ぐ役割を果たします．


## BGMの再生

```csharp
/// <summary>
/// BGMを再生する関数
/// </summary>
/// <param name="bgmName"></param>
public void PlayBGM(string bgmName)
{
    // すでに同じBGMが再生中なら処理しない
    if (currentBGM != null && currentBGM.ToString() == bgmName) return;

    // 指定名のBGMデータを探す
    BGMSoundData data = bgmSoundDatas.Find(data => data.bgmName == bgmName);
    if (data == null)
    {
        Debug.LogWarning($"BGM '{bgmName}' が見つかりませんでした．");
        return;
    }
    
    // AudioClipと音量を設定して再生
    bgmAudioSource.clip = data.audioClip;
    bgmAudioSource.volume = data.volume * bgmMasterVolume * masterVolume;
    bgmAudioSource.Play();

    currentBGM = bgmName;  // 現在のBGM名を記録
}
```
この関数は，指定した名前のBGMを再生する処理を行います．以下のような手順で動作します．

1．すでに同じBGMが再生されているか確認
　現在再生中のBGM（`currentBGM`）と引数で渡されたBGM名（`bgmName`）が同じ場合，無駄に再生し直さないように処理をスキップします．

2．BGMデータを検索
　`bgmSoundDatas`というリストから，指定された名前のBGMデータ（`BGMSoundData`）を探します．

3．AudioClipと音量を設定して再生
　見つけたBGMデータからAudioClipを`bgmAudioSource`に設定し，
　`ボリュームをデータごとの音量` × `BGM用マスターボリューム` × `全体マスターボリューム` 
で計算して設定します．その後，BGMを再生します．

4．現在のBGM名を記録
　次回の再生時に同じBGMか判断できるように，現在再生中のBGM名を`currentBGM`に記録します．

 このように，BGMの重複再生を防ぎつつ，音量調整にも対応した柔軟な再生処理になっています．

## SEの再生
 ```csharp
/// <summary>
/// 効果音を再生する関数
/// </summary>
/// <param name="seName"></param>
public void PlaySE(string seName)
{
    // nullになったGameObjectを削除（破棄されたSEの後処理）
    seGameObjects.RemoveAll(x => x == null);    

    // 同時に鳴らせるSEの上限チェック
    if (seGameObjects.Count > maxSeCount) return;

    // SEデータを検索
    SESoundData data = seSoundDatas.Find(d => d.seName == seName);
    if (data == null)
    {
        Debug.LogWarning($"SE '{seName}' が見つかりませんでした．");
        return;
    }

    // 一時的なGameObjectを生成し，そこにAudioSourceをアタッチ
    GameObject tempGO = new GameObject("TempSE_" + data.audioClip.name);
    AudioSource tempSource = tempGO.AddComponent<AudioSource>();
    tempSource.outputAudioMixerGroup = mixerGroup;

    // SE再生用GameObjectを管理リストに追加し，破棄されないように設定
    seGameObjects.Add(tempGO);
    DontDestroyOnLoad(tempGO);

    // SEのAudioClipと音量を設定して再生
    tempSource.clip = data.audioClip;
    tempSource.volume = data.volume * seMasterVolume * masterVolume;
    tempSource.Play();

    // 再生終了後に自動でGameObjectを破棄
    Destroy(tempGO, data.audioClip.length);
}
 ```
 この関数は，指定した名前のSEを一時的に再生する処理です．以下の手順で動作します．

 1．nullになったGameObjectを削除
　過去に再生されたSE用のGameObjectの中で，すでに破棄された（nullになっている）ものをリストから削除します．これは再生数の上限管理を正しく行うための後始末です．

2．同時再生数の上限チェック
　現在再生中のSEが上限数（`maxSeCount`）を超えていた場合，新たなSEは再生しません．

3．指定されたSEのデータを検索
　`seSoundDatas`リストの中から，引数`seName`と一致する名前のSEデータを探します．

4．SE用の一時的なGameObjectを生成
　再生用に一時的なGameObjectを作り，その中にAudioSourceコンポーネントを追加します．また，ミキサーグループ（mixerGroup）もここで指定されます．
 > 複数のSEを同時に再生できるようにするためSEを鳴らすたびにGameobjectを作成しています

5．SE用GameObjectを管理対象に追加し，シーン切り替えでも破棄されないように設定
　作成したGameObjectをリストに追加し，`DontDestroyOnLoad`を使ってシーン遷移時に削除されないようにします．

6．音源と音量を設定して再生
　AudioClipと，`SEごとの音量` × `SEマスターボリューム` × `全体マスターボリューム` で音量を設定し，再生します．

7．再生後の自動削除
　AudioClipの長さに合わせて，SE再生終了後に一時的なGameObjectを自動で破棄するようにしています．

 このように，この関数は同時再生制御，一時的なオブジェクト管理，音量調整まで含めた効果音の再生処理を安全かつ柔軟に実現しています．

 ```csharp
 /// <summary>
/// シーン名に応じたBGMを再生する
/// </summary>
void PlayBgmBySceneName()
{
    switch (SceneManager.GetActiveScene().name)
    {
        case "TitleScene":
        case "StageSelectScene":
            SoundManager.Instance.PlayBGM("Title");
            break;

        case "Stage1":
            SoundManager.Instance.PlayBGM("Stage1"); break;

        case "Stage2":
            SoundManager.Instance.PlayBGM("Stage2"); break;

        case "Stage3":
            SoundManager.Instance.PlayBGM("Stage3"); break;

        case "Stage4":
            SoundManager.Instance.PlayBGM("Stage4"); break;

        case "Stage5":
            SoundManager.Instance.PlayBGM("Stage5"); break;
    }
}
 ```
 この関数は，現在のシーン名に応じて適切なBGMを再生する処理を行っています．
switch文を使って，シーン名ごとに対応するBGM名を `SoundManager.Instance.PlayBGM(...)` を使って再生します．

　例えば：
　- シーン名が "TitleScene"，"StageSelectScene" のどちらかなら，BGMとして "Title" を再生．
　- シーン名が "Stage1" の場合は "Stage1" という名前のBGMを再生．
　- 同様に "Stage2"～"Stage5" もそれぞれ対応したBGM名を使って再生されます．

このように，シーン遷移に応じて自動的にBGMが切り替わるようにしておくことで，ユーザがシーンを移動するたびに手動でBGMを設定する手間が省けます．ゲーム全体の演出や雰囲気の一貫性を保つのにも役立ちます．

## データクラス定義

```csharp
[System.Serializable]
public class BGMSoundData
{
    public string bgmName;          // BGMの識別名
    public AudioClip audioClip;     // BGMの実データ
    [Range(0, 1)]
    public float volume = 1;        // 個別音量（0〜1）
}

[System.Serializable]
public class SESoundData
{
    public string seName;           // SEの識別名
    public AudioClip audioClip;     // SEの実データ
    [Range(0, 1)]
    public float volume = 1;        // 個別音量（0〜1）
}
```

このコードは，使用するBGMとSE（効果音）の情報をそれぞれまとめて管理するためのデータクラス定義です．

# さいごに
本記事では，機能を追加したどのスクリプトからでも1行でSEを鳴らせるSoundManagerの実装方法を紹介しました．

- Resourcesフォルダとシリアライズされたデータクラスを活用することで，Git追跡しない場合，Inspectorに音源を逐一アタッチする手間を省きました

- シーンごとのBGM自動再生や，SEの重複再生の仕組みも組み込むことで，利便性と柔軟性を両立させました

- 商用音源の取り扱いを配慮し，音源ファイルの再配布リスクを避ける設計としました

このSoundManagerを導入することで，チーム開発でも再利用しやすく，保守性の高い音管理が実現できます．

ぜひ，あなたのプロジェクトにも応用してみてください！