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
を用意し、同時再生することで、個別に音量調整できるようにしている。
コード例
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を用意しておくことで、柔軟な対応ができるので、
この設計と掛け合わせて使うのもおすすめかと思います。