🎵

【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

使用ライブラリ

参考

Discussion