1
1

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

Last updated at Posted at 2024-07-17

nokonobi.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を用意しておくことで、柔軟な対応ができるので、
この設計と掛け合わせて使うのもおすすめかと思います。

1
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
1
1