ゲームジャムに特化したSoundManagerの作り方

![nokonobi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96808/87d4ce24-9dd6-0d41-e3de-36a7a2b71a50.png)


Unity標準搭載のオーディオ機能向けで、
ゲームジャムに特化したSoundManagerの作り方を紹介します。

# ゲームジャムとして必要な要素
- すぐ更新・すぐ再生
- とにかく動くこと優先
- なるべく手間をかけないで更新
- 再利用性は考えない
- 難しいことはしない
- ゲームに合わせた調整は可能な形にしておく

## すぐ更新・すぐ再生
```
public List<AudioClip> audioClips
```
を使う
波形をすぐ使える
追加が簡単

## とにかく動くこと優先
エラーや警告は出さない
音が鳴ることを優先
リソースの最適化とかは考えない

## なるべく手間をかけない
singletonを使う　ゲームのどこからでも再生可能な形にする。

ゲームの任意の場所から
```
SoundManager.Instance.PlayXXX();
```
みたいに呼べば音が鳴る

## 再利用性は考えない
ゲームごとにオーダーメードで関数を用意する

## 難しいことはしない
3D再生とか、ループ制御とか難しくなりそうなことは
初手ではなるべく避ける。
※音が鳴ることを優先
※後から余裕があったら3Dとか特殊制御は対応できるようにしておく

## ゲームに合わせた調整は可能な形にしておく
開発終盤などで、調整したくなった場合に、調整可能な形にしておく
ゲーム固有の機能追加も可能な形に、緩めの設計で作っていく
せっかくゲームジャムなので、面白機能にチャレンジしても良い。

ので、
再生イベント（キュー）毎に
```
PlayXXX()
```
といったpublic 関数を用意して、
プログラムから直接音を指定しない形にしている。

こうすることで、ゲームジャム中に
- BGMの音量を変えたいとか
- 後からランダム再生にしてみたい
- 音を差し替えたい

といった時に、SoundManager.csのみ更新すればよい形になり、
gitの更新などのコンフリクトを避けることができる。


## 波形データはAssets/auido/に出力する
DAWでの出力先をUnityのaudioフォルダにして、
直接出力できるようにしておく。

unityで開けば、自動的に.metaが生成される。

ポイント：
- コピー処理すらしない
- 上書き更新など楽

## SoundManager.csを持つ　SoundManagerゲームオブジェクトをシーンに用意
SoundManagerはゲームで唯一一つ存在し、
破棄しない。ゲームの開始シーンなどに配置する。
```
        DontDestroyOnLoad(this);
        if (Instance != null)
        {
            return;
        }

        Instance = this;
```


SoundManager.csのあるゲームオブジェクトをプレファブ化しておくと良い。
audioに波形を追加したら、
SoundManagerのインスペクタでAudioClipに追加し、
追加した順でSoundKeyを作っておくと、あとでコードを見た時にも困らない。
```
    enum SoundKey
    {
        meow,
        countDown,
        eat,
        toy
    };
```
※時間なくて面倒ならindex数字直打ちでも良い。


# コード説明
上の方針で作った例

このゲームでは猫が伸びるゲームで、
タイトル、ゲーム、ゲームオーバー（ウロボロス）、リザルト画面があり、
それぞれBGMがある。

## SE
おもちゃ音、猫鳴き声、餌を食べる音、
死亡音、クリア音、
猫が伸びる時の音
などがある。
audioClips に効果音系を入れておく

## SEの最大同時発音数は16個
```
List<AudioSource> audioSources
```
で巡回して鳴らす形にしている


## 猫の鳴き声はランダム再生
```
int rndval = Random.Range(0, 8);
```
switchで再生音を切り替えている。
default:が基本鳴る音で、たまに他の音がなるといった
ランダム再生の重みももたせている。


## 猫が伸びる音は無限音階の音で、継続再生するような仕組みを入れていて
伸び続けている場合は、鳴り続け、
伸びが止まった時は、音を止めるけど
音がぷつぷつ途切れないように、フェードアウトして
ポーズして止めている。
```
audioSourceMugenOnkai
```

## BGMの再生は
ゲーム開始時の初回にカウントダウン（イントロ）が入ってから、
ループのBGMが流れる仕組みにしている。
```
AudioSource audioSourceBgm
```

また、BGMのレイヤー再生として、
```
AudioSource audioSourceBgmChorus
```
を用意し、同時再生することで、個別に音量調整できるようにしている。

# コード例

``` SoundManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;

public class SoundManager : MonoBehaviour
{
    public static SoundManager Instance;

    public List<AudioClip> audioClips = new List<AudioClip>();

    List<AudioSource> audioSources = new List<AudioSource>();


    public List<AudioClip> audioClipsBgm = new List<AudioClip>();
    private AudioSource audioSourceBgm;
    private AudioSource audioSourceBgmChorus;
    private AudioSource audioSourceMugenOnkai;

    enum SoundKey
    {
        meow,
        countDown,
        eat,
        gong,
        meow2,
        meow3,
        meow4,
        mugenonkai,
        toy
    };

    enum SoundBgmKey
    {
        main,
        catPurring,
        mainLevel2,
        uroboros,
        mainLevel2Chorus,
        nekonobi
    };

    void Start()
    {
        DontDestroyOnLoad(this);
        if (Instance != null)
        {
            return;
        }

        Instance = this;

        for (int i = 0; i < 16; i++)
        {
            audioSources.Add(gameObject.AddComponent<AudioSource>());
        }

        audioSourceBgmChorus= gameObject.AddComponent<AudioSource>();
        audioSourceBgmChorus.volume = 0.35f;
        
        audioSourceBgm = gameObject.AddComponent<AudioSource>();
        audioSourceBgm.volume = 0.35f;
        audioSourceMugenOnkai = gameObject.AddComponent<AudioSource>();
        
        audioSourceMugenOnkai.loop = true;
        audioSourceMugenOnkai.clip = audioClips[(int)SoundKey.mugenonkai];

    }

    void Update()
    {
        if (audioSourceMugenOnkai == null)
        {
            return;
        }
        
        if (targetVolume > 0)
        {
            targetVolume = targetVolume - 0.06f;
            audioSourceMugenOnkai.volume = targetVolume;
        }
        else
        {
            audioSourceMugenOnkai.Pause();
        }
    }
    
    public void PlayNobi()
    {
        targetVolume = 1.0f;
        
        audioSourceMugenOnkai.UnPause();

        if (audioSourceMugenOnkai.isPlaying == false)
        {
            audioSourceMugenOnkai.Play();
        }
    }

    private bool isNobiPlay = false;
    private float targetVolume = 0.0f;
    
    async UniTask PlayNobiAsync()
    {
        audioSourceMugenOnkai.Play();
        isNobiPlay = true;
        await UniTask.Delay(TimeSpan.FromSeconds(0.1f));
        audioSourceMugenOnkai.Pause();
        isNobiPlay = false;
    }


    private int playbackCount = 0;

    void Play(SoundKey soundKey)
    {
        playbackCount++;
        playbackCount = playbackCount % 16;
        audioSources[playbackCount].clip = audioClips[(int)soundKey];
        audioSources[playbackCount].Play();
    }

    /// <summary>
    /// 猫の鳴き声
    /// </summary>
    public void PlayMeow()
    {
        int rndval = Random.Range(0, 8);

        switch (rndval)
        {
            case 0:
                Play(SoundKey.meow);
                break;

            case 1:
                Play(SoundKey.meow2);
                break;

            case 2:
                Play(SoundKey.meow3);
                break;

            case 3:
                Play(SoundKey.meow4);
                break;

            default:
                Play(SoundKey.meow);
                break;
        }
    }

    /// <summary>
    /// 猫が食べた
    /// </summary>
    public void PlayEat()
    {
        Play(SoundKey.eat);
    }
    
    public void PlayToy()
    {
        Play(SoundKey.toy);
    }

    /// <summary>
    /// クリア時のゴング
    /// </summary>
    public void PlayGong()
    {
        Play(SoundKey.gong);
    }

    /// <summary>
    /// 猫のゴロゴロ鳴き
    /// </summary>
    public void PlayCatPurring()
    {
        audioSourceBgm.clip = audioClipsBgm[(int)SoundBgmKey.catPurring];
        audioSourceBgm.loop = true;
        audioSourceBgm.Play();
    }

    public void PlayTitleBgm()
    {
        audioSourceBgmChorus.clip = audioClipsBgm[(int)SoundBgmKey.mainLevel2Chorus];
        audioSourceBgmChorus.loop = true;
        audioSourceBgmChorus.Play();
    }


    /// <summary>
    /// 猫のゴロゴロ鳴きを止める
    /// </summary>
    public void StopCatPurring()
    {
        audioSourceBgm.Stop();
        audioSourceBgmChorus.Stop();
    }

    /// <summary>
    /// ゲーム開始のカウントダウン
    /// </summary>
    public void PlayCountDown()
    {
        Play(SoundKey.countDown);
    }

    /// <summary>
    /// ゲーム中のBGM
    /// </summary>
    public void PlayBgm()
    {
        PlayBgmAsync().Forget();
    }

    async UniTask PlayBgmAsync()
    {
        audioSourceBgm.volume = 0.35f;
        audioSourceBgm.Stop();
        SoundManager.Instance.PlayCountDown();

        await UniTask.Delay(TimeSpan.FromSeconds(1));
        audioSourceBgm.Stop();
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        audioSourceBgm.Stop();
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        audioSourceBgm.Stop();
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        audioSourceBgm.Stop();
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        audioSourceBgm.Stop();
        
        audioSourceBgm.clip = audioClipsBgm[(int)SoundBgmKey.nekonobi];
        audioSourceBgm.loop = true;
        audioSourceBgm.Play();
    }

    /// <summary>
    /// BGM止める
    /// </summary>
    public void StopBgm()
    {
        audioSourceBgm.Stop();
        audioSourceBgmChorus.Stop();
    }

    /// <summary>
    /// ゲーム中のBGM Level2
    /// </summary>
    public void PlayBgmLevel2()
    {
        audioSourceBgm.volume = 0.25f;
        audioSourceBgm.Stop();
        audioSourceBgm.clip = audioClipsBgm[(int)SoundBgmKey.mainLevel2];
        audioSourceBgm.loop = true;
        audioSourceBgm.Play();
    }

    /// <summary>
    /// やられた時
    /// </summary>
    public void PlayDeath()
    {
        audioSourceBgm.Stop();
        // Play(SoundKey.gong);

        audioSourceBgm.volume = 1.0f;
        audioSourceBgm.clip = audioClipsBgm[(int)SoundBgmKey.uroboros];
        audioSourceBgm.loop = false;
        audioSourceBgm.Play();
    }
}

```

# デメリット
ゲームジャム向きということで、
- メンテナンス性が犠牲（スパゲッティになりがち）
- 大量なデータを扱う場合、手間が多すぎる（オーダーメード関数ラッパー）
- メモリをどかどか使う（音数が多い長尺の音が多い場合）
- 起動が遅くなる（音数が多い長尺の音が多い場合）
といったデメリットがあります。

音数が多い場合はサウンドミドルウェアを使う方が
あとあとの調整や管理の時に楽にできます。

ただ、別の管理ツールとそのワークフローの習得が必要になるので
ゲームジャムだとちょっと使いずらいところも。
（慣れている人には問題ないですが）

ミドルウェアを使う場合でも、
アプリ側での特殊な再生をしたい場合などに
SoundManagerを用意しておくことで、柔軟な対応ができるので、
この設計と掛け合わせて使うのもおすすめかと思います。