i-school - SoundManagerでゲーム内の音源を管理する

SoundManagerクラスの設計 〜ゲーム内の音源を一元管理する方法〜 //


 Unityにおいて音楽の再生にはAudioSourceコンポーネントが用いられます。AudioClipに再生したいBGMやSEを設定して、再生をさせる手順です。
この手法ですと、各シーンにBGM用のゲームオブジェクトを用意したり、また各ゲームオブジェクトごとにSE用のAudioSourceコンポーネントをアタッチして設定するため
多くの手間がかかるだけではなく、どこで何の音源の管理を行っているのか不透明になり、全体的に音源について煩雑になりがちです。

 SoundManagerはそれらの音源にかかわる処理を一手に引き受けて、音源ファイルの管理/登録、音源の再生/停止をさせるための管理用のクラスです。
作成するゲームオブジェクトはプロジェクト全体で1つで済むようになり、再生などの処理もこのクラスのみで実行できます。


 またSoundManagerは汎用クラスです。一度スクリプト作成すれば、他のすべてのプロジェクトで利用が可能です。


 内容的には、音源再生用のAudioSourceコンポーネントを、BGM用・SE用といった形で種別に合わせて複数用意し、
それらのAudioClipの部分(鳴らすべき音源)を、状況に合わせてプログラムから差し替えて再生してくれるというロジックです。


実装手順


 以下の手順で実装を行います。

 1.SoundManagerスクリプトを作成する
 2.SoundManagerゲームオブジェクトを作成し、SoundManagerスクリプトをアタッチする
 3.Enumの登録方法、音源の登録方法
 4.再生/停止処理の実行方法
 5.AudioMixerについて


1.SoundManagerスクリプトを作成する


 SoundManagerという、BGMやSEをまとめて管理するクラスを作成します。C#Script を作成し、名前をSoundManagerに変更します。

 こちらのクラスはシングルトン・デザインパターンになっています。
シングルトンにすることによって、いずれのクラスからでもBGMやSEの再生を命令しやすい設計にしています。


<シングルトンデザインパターン>


 シングルトンとは、数多くあるデザインパターンの1つです。そのクラスのインスタンスが必ず1つであることを保証するデザインパターンのことを言います。

 SoundManagerクラスでは、このシングルトンを採用しています。つまり、ゲーム中を通じて、このSoundManagerクラスが1つしか存在できないようになります。
実装例は複数ありますが、一番読みやすい方式で記述しています。

 このシングルトンによってインスタンスが1つか生成されないことが保証されますので、このSoundManagerクラスへの参照は、いずれのクラスからであっても変数を介さずに参照を行えるようになります。

 例えば、Enemyというクラスがあり、そのEmenyクラスを持つゲームオブジェクトが5つあった場合、「どの」Enemyクラスであるかを確定できないと、対象となるEnemyクラスへは参照できません。
そのため、Enemy型の変数を用意して、その変数へ参照したいEnemyクラスを代入することによって、はじめてEnemyクラスの情報を扱うことができるようになります。

 ですがシングルトンであるSoundManagerクラスの場合には、このインスタンスは常に1つしかないことが保証されていますので、「どの」という指定の部分が不要になります。
つまり変数への代入が不要になります。
SoundManagerという指定はすなわち、自動的にただ1つのSoundManagerへの参照が行われます。


SoundManager スクリプトを作成する


 設計ですが、AudioClip(鳴らしたい音源)をインスペクター上で登録できるようにしておいて、それを適宜AudioSourseコンポーネントのAudioClipにアサインして使用するようにします。
例えるなら、AudioClipはCD(音源)であり、AudioSourseがCDプレイヤー(再生アプリ)、AudioSourseのAudioClipは再生したいCDを入れる場所(ライブラリ)、という役割のイメージで使用しています。

 またDOTweenのDoFadeメソッドを利用して、BGMを切り替える際には、現在再生中のBGMの音量を小さくしつつ、その間に次のBGMが鳴り始めるという、いわゆるクロスフェード機能を実装しています。
これによって急にBGMが変わってしまうことを防ぎ、ナチュラルな形でBGMとBGMとを繋げて鳴らすことができます。

using System.Collections;
using UnityEngine;
using DG.Tweening;
using UnityEngine.Audio;

/// <summary>
/// 音源管理クラス
/// </summary>
public class SoundManager : MonoBehaviour
{
    public static SoundManager instance;

    // BGM管理
    public enum BGM_Type
    { 
        // BGM用の列挙子をゲームに合わせて登録

        SILENCE = 999,        // 無音状態をBGMとして作成したい場合には追加しておく。それ以外は不要
    }

    // SE管理
    public enum SE_Type
    {
        // SE用の列挙子をゲームに合わせて登録
    }

    // クロスフェード時間
    public const float CROSS_FADE_TIME = 1.0f;

    // ボリューム関連
    public float BGM_Volume = 0.1f;
    public float SE_Volume = 0.2f;
    public bool Mute = false;

    // === AudioClip ===
    public AudioClip[] BGM_Clips;
    public AudioClip[] SE_Clips;

    // SE用AudioMixer  未使用
    public AudioMixer audioMixer;


    // === AudioSource ===
    private AudioSource[] BGM_Sources = new AudioSource[2];
    private AudioSource[] SE_Sources = new AudioSource[16];

    private bool isCrossFading;

    private int currentBgmIndex = 999;

    void Awake()
    {
        // シングルトンかつ、シーン遷移しても破棄されないようにする
        if (instance == null) {
            instance = this;
            DontDestroyOnLoad(gameObject);         
        } else  { 
            Destroy(gameObject);
        }

        // BGM用 AudioSource追加
        BGM_Sources[0] = gameObject.AddComponent<AudioSource>();
        BGM_Sources[1] = gameObject.AddComponent<AudioSource>();

        // SE用 AudioSource追加
        for (int i = 0; i < SE_Sources.Length; i++)
        {
            SE_Sources[i] = gameObject.AddComponent<AudioSource>();
        }
    }

    void Update() 
    {
        // ボリューム設定
        if (!isCrossFading) {
            BGM_Sources[0].volume = BGM_Volume;
            BGM_Sources[1].volume = BGM_Volume;
        }

        foreach (AudioSource source in SE_Sources)
        {
            source.volume = SE_Volume;
        }
    }

    /// <summary>
    /// BGM再生
    /// </summary>
    /// <param name="bgmType"></param>
    /// <param name="loopFlg"></param>
    public void PlayBGM(BGM_Type bgmType, bool loopFlg = true)
    {
        // BGMなしの状態にする場合            
        if ((int)bgmType == 999) {    
            StopBGM();           
            return;                 
        }
 
        int index = (int)bgmType;
        currentBgmIndex = index;

        if (index < 0 || BGM_Clips.Length <= index)
        {
            return;
        }

        // 同じBGMの場合は何もしない
        if (BGM_Sources[0].clip != null && BGM_Sources[0].clip == BGM_Clips[index])  {
            return;
        } else if (BGM_Sources[1].clip != null && BGM_Sources[1].clip == BGM_Clips[index]) {
            return;
        }

        // フェードでBGM開始
        if (BGM_Sources[0].clip == null && BGM_Sources[1].clip == null)  {
            BGM_Sources[0].loop = loopFlg;
            BGM_Sources[0].clip = BGM_Clips[index];
            BGM_Sources[0].Play();
        } else {
            // クロスフェード処理
            StartCoroutine(CrossFadeChangeBMG(index, loopFlg));
        }
    }

    /// <summary>
    /// BGMのクロスフェード処理
    /// </summary>
    /// <param name="index">AudioClipの番号</param>
    /// <param name="loopFlg">ループ設定。ループしない場合だけfalse指定</param>
    /// <returns></returns>
    private IEnumerator CrossFadeChangeBMG(int index, bool loopFlg) {
        isCrossFading = true;
        if (BGM_Sources[0].clip != null)  {
            // [0]が再生されている場合、[0]の音量を徐々に下げて、[1]を新しい曲として再生
            BGM_Sources[1].volume = 0;
            BGM_Sources[1].clip = BGM_Clips[index];
            BGM_Sources[1].loop = loopFlg;
            BGM_Sources[1].Play();
            BGM_Sources[1].DOFade(1.0f, CROSS_FADE_TIME).SetEase(Ease.Linear);
            BGM_Sources[0].DOFade(0, CROSS_FADE_TIME).SetEase(Ease.Linear);

            yield return new WaitForSeconds(CROSS_FADE_TIME);
            BGM_Sources[0].Stop();
            BGM_Sources[0].clip = null;
        } else {
            // [1]が再生されている場合、[1]の音量を徐々に下げて、[0]を新しい曲として再生
            BGM_Sources[0].volume = 0;
            BGM_Sources[0].clip = BGM_Clips[index];
            BGM_Sources[0].loop = loopFlg;
            BGM_Sources[0].Play();
            BGM_Sources[0].DOFade(1.0f, CROSS_FADE_TIME).SetEase(Ease.Linear);
            BGM_Sources[1].DOFade(0, CROSS_FADE_TIME).SetEase(Ease.Linear);

            yield return new WaitForSeconds(CROSS_FADE_TIME);
            BGM_Sources[1].Stop();
            BGM_Sources[1].clip = null;
        }
        isCrossFading = false;
    }

    /// <summary>
    /// BGM完全停止
    /// </summary>
    public void StopBGM()
    {
        BGM_Sources[0].Stop();
        BGM_Sources[1].Stop();
        BGM_Sources[0].clip = null;
        BGM_Sources[1].clip = null;
    }

    /// <summary>
    /// SE再生
    /// </summary>
    /// <param name="seType"></param>
    public void PlaySE(SE_Type seType)  {
        int index = (int)seType;
        if (index < 0 || SE_Clips.Length <= index) {
            return;
        }

        // 再生中ではないAudioSourceをつかってSEを鳴らす
        foreach (AudioSource source in SE_Sources) {

            // 再生中の AudioSource の場合には次のループ処理へ移る
      if (source.isPlaying) {
              continue;
          }

            // 再生中でない AudioSource に Clip をセットして SE を鳴らす
          source.clip = SE_Clips[index];
          source.Play();
            break;
        }
    }

    /// <summary>
    /// SE停止
    /// </summary>
    public void StopSE() {
        // 全てのSE用のAudioSourceを停止する
        foreach (AudioSource source in SE_Sources) {
            source.Stop();
            source.clip = null;
        }
    }

    /// <summary>
    /// BGM一時停止
    /// </summary>
    public void MuteBGM()
    {
        BGM_Sources[0].Stop();
        BGM_Sources[1].Stop();
    }

    /// <summary>
    /// 一時停止した同じBGMを再生(再開)
    /// </summary>
    public void ResumeBGM()
    {
        BGM_Sources[0].Play();
        BGM_Sources[1].Play();
    }


////* 未使用 *////


    /// <summary>
    /// AudioMixer設定
    /// </summary>
    /// <param name="vol"></param>
    public void SetAudioMixerVolume(float vol)
    {
        if (vol == 0)
        {
            audioMixer.SetFloat("volumeSE", -80);
        }
        else
        {
            audioMixer.SetFloat("volumeSE", 0);
        }
    }
}


2.SoundManagerゲームオブジェクトを作成し、SoundManagerスクリプトをアタッチする


 ヒエラルキー上で右クリックし、Create Empty を選択します。空のゲームオブジェクトが作成されますので、SoundManagerに名前を変えます。

 Project内のSoundManagerスクリプトをドラッグアンドドロップして、SoundManagerゲームオブジェクトにアタッチします。


ヒエラルキー画像(単体でヒエラルキーに配置する)



SoundManagerゲームオブジェクトのインスペクター画像



3.Enumの登録方法、音源の登録方法

 
 冒頭にも述べましたように、SoundManagerクラスは汎用クラスです。そのためプロジェクトに合わせたBGMやSEの種類を設定して登録する必要があります。

 まずゲーム内で使用する音源について、使用するシーンやSEを鳴らす場所に合わせてBGMとSEのEnum列挙子を登録します。
例えば、以下のように登録します。この列挙子を指定して音源を再生します。

    // BGM管理
    public enum BGM_Type
    { 
        Title,
        Home,
        Battle
    }

    // SE管理
    public enum SE_Type
    {
        OK,
        NG,
        ItemGet,
        Damage
    }

 列挙子を登録したら、列挙子と同数のAudloClip配列をSoundManagerのインスペクターにて用意します。
BGMはBGM_Clips変数、SEはSE_Clips変数です。

 今回の例では、BGM_ClipsのSize 3、SE_Clips のSize 4 で用意します。

BGM_ClipsのSize 0 、SE_Clips のSize 0


   ↓

BGM_ClipsのSize 3 、SE_Clips のSize 4



 Unityには事前に音源ファイルをインポートし、Audioフォルダ内にフォルダ分けして用意しておきます。


Audioフォルダ構成



 各音源ファイル名は、列挙子と同名になるように名前を変更しておきます。


Audio/BGMフォルダ


Audio/SEフォルダ



 先ほど用意したBGM_Clips変数とSE_Clips変数の配列の要素に、Enumで設定した列挙子と同じ順番で音源ファイルをアサインします。

アサイン後のインスペクター画像


 これで使用する準備は完了です。


4.再生/停止処理の実行方法


 SoundManagerのPlayBGMメソッド、あるいはPlaySEメソッドを呼び出すことで音源を再生できます。
シングルトンですので、アサインは不要で、いずれのクラスからでも以下の命令で呼び出しが可能です。


再生


 引数にそれぞれのEnumを指定していますので、Enumの列挙子の指定を変更すればこの2つの命令だけで音源を再生できます。

BGMの場合
    SoundManager.instance.PlayBGM(SoundManager.BGM_Type.Title);   <= 引数を変える

SEの場合
    SoundManager.instance.PlaySE(SoundManager.SE_Type.OK);      <= 引数を変える

 なお音声を再生させる場合には、同じようにEnumのVoice_TypeとVoice_Clips変数を用意すれば対応できます。


停止

 
 停止させたい場合には以下の命令を呼び出します。

BGMの場合
    SoundManager.instance.StopBGM();

SEの場合
    SoundManager.instance.StopSE();


一時停止/再開

 
 BGMのみ、停止後に同じBGMを再生させるメソッドを用意しています。

一時停止
    SoundManager.instance.MuteBGM();


同じBGM再開
 MuteBGMメソッド実行後に
    SoundManager.instance.ResumeBGM();


5.AudioMixerについて


 SEやBGMの音量がバラバラで上手く調整が出来ない場合には、AudioMixer機能を利用します。(using UnityEngine.Audio は、AudioMixerのための宣言です。)
設定は用意していますが実装はしていませんので、もしも調整が上手くいかない場合には、この機能の利用を検討してください。

  => AudioMixer の機能と活用方法
     AudioMixer の実装例
     スクリプトによる AudioMixer の音量制御方法



 他にも多くの記事がありますので、参考に実装をしてみてください。こちらのサイト様も詳しいです。

10ANTZ Developers Blog様
AudioMixerを活用して、サウンド調整の処理
https://developers.10antz.co.jp/archives/609
ゴイサギ日記様
【Unity】サウンド AudioClip と AudioMixer
https://goisagi-517.hatenablog.com/entry/2019/07/2...
ゼニガネブログ様
【Unity】AudioMixerで遊んでみよう!(1/3)
https://zenigane138.hateblo.jp/entry/2018/02/20/22...