🎵
【Unity】BGMフェード実装 & 処理の抽象化【C#】
背景と補足
- BGMのフェードイン,アウト、クロスフェード機能が欲しい(Unity標準機能にはない)
- 本筋以外は適宜省略 or 簡単に表現している(ロードや
CancellationToken
まわり)
方針
全体の方針
-
FadeIn()
,FadeOut()
,CrossFade()
の3関数を作る - BGM用の
AudioSource
を2つ用意し、フェード内容に応じて使い分ける
private (AudioSource active, AudioSource inactive) bgmSources;
各関数の方針
public async UniTask FadeIn(string resourceAddress, float duration, float volume)
- 再生中でない
AudioSource
を用いる - 指定サウンド
resourceAddress
を指定秒duration
で指定音量volume
まで段々大きくする
public async UniTask FadeOut(float duration)
- 再生中の
AudioSource
を用いる - 指定秒
duration
で音量0まで段々小さくする
public async UniTask CrossFade(string resourceAddress, float duration)
- 両方の
AudioSource
を用いる - 再生中のBGMを指定秒
duration
で音量0にすると同時に、指定サウンドresourceAddress
の音量を1.0f
まで段々大きくする
実装したが…(抽象化前)
FadeIn()
public async UniTask FadeIn(string resourceAddress, float duration, float volume)
{
//この辺にリソースロード処理
bgmSources.active.clip = clip; //ロードしたサウンドをセット
bgmSources.active.volume = 0f;
bgmSources.active.Play();
//フェードイン実行
float elapsedTime = 0f;
while (elapsedTime < duration)
{
float t = elapsedTime / duration;
bgmSources.active.volume = Mathf.Lerp(0f, volume, t);
elapsedTime += Time.deltaTime;
await UniTask.Yield();
}
bgmSources.active.volume = volume;
}
FadeOut()
public async UniTask FadeOut(float duration)
{
float startVol = bgmSources.active.volume;
//フェードアウト実行
float elapsedTime = 0f;
while (elapsedTime < duration)
{
float t = elapsedTime / duration;
bgmSources.active.volume = Mathf.Lerp(startVol, 0f, t);
elapsedTime += Time.deltaTime;
await UniTask.Yield();
}
bgmSources.active.volume = 0f;
bgmSources.active.Stop();
bgmSources.active.clip = null;
}
CrossFade()
public async UniTask CrossFade(string resourceAddress, float duration)
{
//この辺にリソースロード処理
bgmSources.inactive.clip = clip; //ロードしたサウンドをセット
bgmSources.inactive.volume = 0f;
bgmSources.inactive.Play();
//クロスフェード実行
float elapsedTime = 0f;
while (elapsedTime < duration)
{
float t = elapsedTime / duration;
bgmSources.active.volume = Mathf.Lerp(1f, 0f, t);
bgmSources.inactive.volume = Mathf.Lerp(0f, 1f, t);
elapsedTime += Time.deltaTime;
await UniTask.Yield();
}
bgmSources.active.Stop();
bgmSources = (bgmSources.inactive, bgmSources.active); //active/inactiveを入れ替え
currentBGMAddress = resourceAddress;
}
- 3関数全てで、
while
文で指定秒duration
までのelapsedTime
計測処理を記述している - この処理に手を加える場合、3関数分の変更が必要になり、保守性が悪い
処理の抽象化
-
while
処理を3関数から削除し、代わりにExecuteVolumeTransition()
として新たに定義 - 3関数はフェード内容を定義し、
ExecuteVolumeTransition()
を利用するだけの形に
ExecuteVolumeTransition()
の方針
private async UniTask ExecuteVolumeTransition(float duration, Action<float> onProgress, Action onComplete = null)
-
while
文で指定秒duration
までのelapsedTime
計測処理を行う -
Action<float> onProgress
: 計測中、elapsedTime / duration
を用いて何をするか
実装した
private async UniTask ExecuteVolumeTransition(float duration, Action<float> onProgress,
Action onComplete = null)
{
float elapsedTime = 0f;
while (elapsedTime < duration)
{
float t = elapsedTime / duration;
onProgress(t);
elapsedTime += Time.deltaTime;
await UniTask.Yield();
}
onProgress(1.0f);
onComplete?.Invoke();
}
FadeIn()
, FadeOut()
, CrossFade()
はこうなった
FadeIn()
public async UniTask FadeIn(string resourceAddress, float duration, float volume)
{
//この辺にリソースロード処理
bgmSources.active.clip = clip;
bgmSources.active.volume = 0;
bgmSources.active.Play();
await ExecuteVolumeTransition(
duration,
progressRate => bgmSources.active.volume = Mathf.Lerp(0f, volume, progressRate)
);
}
FadeOut()
public async UniTask FadeOut(float duration)
{
float startVol = bgmSources.active.volume;
await ExecuteVolumeTransition(
duration,
progressRate => bgmSources.active.volume = Mathf.Lerp(startVol, 0.0f, progressRate)
);
bgmSources.active.Stop();
bgmSources.active.clip = null;
}
CrossFade()
public async UniTask CrossFade(string resourceAddress, float duration)
{
//この辺にリソースロード処理
bgmSources.inactive.clip = clip;
bgmSources.inactive.volume = 0f;
bgmSources.inactive.Play();
await ExecuteVolumeTransition(
duration,
progressRate =>
{
bgmSources.active.volume = Mathf.Lerp(1f, 0f, progressRate);
bgmSources.inactive.volume = Mathf.Lerp(0f, 1f, progressRate);
},
() => //onComplete
{
bgmSources.active.Stop();
bgmSources = (bgmSources.inactive, bgmSources.active);
currentBGMAddress = resourceAddress;
});
}
つまり、どういうこと
- 処理を
ExecuteVolumeTransition()
に抽出し、以下のメリット~- 各フェード関数がシンプルになり、読みやすくなった(
CrossFade()
はそうでもないかも) - 音量変化のロジックを一箇所に集約できた
- 同じ処理を複数回書く必要がなくなり、ミスや冗長さを防げる
- フェード処理の改善や機能追加が簡単になった
-
ExecuteVolumeTransition()
だけ修正すればOK
-
- 各フェード関数がシンプルになり、読みやすくなった(
使用ライブラリ
- UniTask by Cysharp (MIT License)
Discussion