自作ゲームにイントロ付きループBGM再生機能を付ける

今回は久しぶりにBGMに関することを記載したいと思います

ゲームをプレイしているとき、BGMによってはイントロがあるタイプがあります

今回はイントロを再生したのちループBGMを再生する制御についてみていきたいと思います

コンテンツ

要件と前提条件

今回の要件と前提条件はこうなります

前提条件
  • イントロ音源とループ音源の2つが存在する
  • BGMの切り替わりを滑らかにするためにフェードで切り替えたい
要件
  • イントロ音源が存在する場合そちらを優先して再生
  • BGMを切り替える場合フェードを行う

このような要件になります

実装

実装ですが、イントロ実装はイントロ音源とループ音源が分かれている場合は比較的簡単に実装できます

まずイントロ用とループ用の2つのオーディオを持ちますそしてイントロ即再生し、

ループ用を遅延再生します

遅延実行は公式によると以下で実装できます

引数に遅延時間を要求されますが時間はBGMの以下で取得することができます

これでイントロを実装する材料ができました

↓ということでシンプルに実装するとこうなります

// オーディオ
public AudioSource m_audioIntro;
public AudioSource m_audioLoop;

// BGMデータ
public AudioClip m_intro;
public AudioClip m_loop;


public void Play()
{
    m_audioIntro.clip = m_intro;
    m_audioLoop.clip  = m_loop;
    m_audioIntro.Play();
    m_audioLoop.PlayDelayed(m_intro.length);
}

これでイントロを再生したと同時にループを遅延実行することができます

BGMをイントロとループに分けたい場合は音源を加工する必要があります

加工方法はこちらを参考にしました

https://www.ay3s-room.com/entry/soundengine-compilation1

※BGMの加工なので切り分ける場合は加工OKなBGMにしましょう

プラスアルファでフェード切り替えを実装する

ここにさらに再生したBGMを切り替えることを考えたいと思います

実装する前に同じBGMかどうか判断する必要があります

そのためBGMをScriptableObjectにまとめてしまいIDを設定できるようにします

↓BGMのデータ定義

using UnityEngine;

/// <summary>
/// BGMデータ
/// </summary>
[CreateAssetMenu(menuName = "Data/Create BgmData")]
public class BgmData : ScriptableObject
{
	// ID
	[SerializeField]
	private string m_id = "";

	// イントロBGM
	[SerializeField]
	private BgmSet m_bgmIntro = null;

	// ループBGM
	[SerializeField]
	private BgmSet m_bgmLoop = null;

	public string Id => m_id;

	public BgmSet BgmIntro => m_bgmIntro.BGM != null ? m_bgmIntro : m_bgmLoop;

	public BgmSet BgmLoop => m_bgmLoop;
}

[System.Serializable]
public class BgmSet
{
	// レート
	[SerializeField]
	private float m_rate = 1.0f;

	// BGM
	[SerializeField]
	private AudioClip m_bgm = null;

	public float Rate => m_rate;

	public AudioClip BGM => m_bgm;
}

その後、BGM再生機能にこのデータを丸々渡してあげます

そのBGM再生機能がこうなります

↓BGM再生機能

using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class BgmPlayer : MonoBehaviour
{
	// ------------------------------------------------
	// 定数

	private static readonly float MaxRate = 1.0f;

	public enum Phase
	{
		None,
		FadeOut,
		FadeIn,
	}

	// ------------------------------------------------
	// 設定項目

	// イントロ
	[SerializeField]
	private VolumeChanger m_intro = null;

	// ループ
	[SerializeField]
	private VolumeChanger m_loop = null;

	// フェード時間
	[SerializeField]
	private float m_fadeTime = 0.5f;

	// ------------------------------------------------
	// メンバ変数

	// 再生中のID
	private string m_id = "";

	// 時間
	private float m_time = 0.0f;

	// 切り替えフェーズ
	private Phase m_phase = Phase.None;

	// BGMデータ
	private BgmData m_fadingBgmData = null;

	private void Awake()
	{
		this.UpdateAsObservable()
			.Where((_) => m_phase != Phase.None && !GameMainManager.Instance.IsStop)
			.Subscribe((_) => PlayFade())
			.AddTo(this);
	}

	/// <summary>
	/// 再生する
	/// </summary>
	/// <param name="bgmData">BGMデータ</param>
	public void Play(BgmData bgmData)
	{
		if (string.IsNullOrEmpty(m_id))
		{
			m_fadingBgmData = bgmData;
			ChangeBGM(MaxRate, m_fadingBgmData.Id, m_fadingBgmData);
		}
		else if (m_id == bgmData.Id)
		{
			return;
		}
		else
		{
			switch (m_phase)
			{
				case Phase.None:
					m_time = 0.0f;
					m_phase = Phase.FadeOut;
					break;
				case Phase.FadeOut:
					break;
				case Phase.FadeIn:
					if (m_id != bgmData.Id)
					{
						m_phase = Phase.FadeOut;
					}
					break;
			}
			m_fadingBgmData = bgmData;
		}
	}

	/// <summary>
	/// BGMをフェード再生する
	/// </summary>
	private void PlayFade()
	{
		m_time += Time.deltaTime;

		var id = m_phase switch
		{
			Phase.FadeIn => m_fadingBgmData.Id,
			Phase.FadeOut or _ => m_id,
		};

		if (m_time > m_fadeTime)
		{
			if (m_phase == Phase.FadeOut)
			{
				m_phase = Phase.FadeIn;
				m_time = 0.0f;
			}
			else if (m_phase == Phase.FadeIn)
			{
				m_phase = Phase.None;
				m_time = m_fadeTime;
			}

		}
		ChangeBGM(m_time / m_fadeTime * MaxRate, id, m_fadingBgmData);
	}

	/// <summary>
	/// BGMを変える
	/// </summary>
	/// <param name="rate">レート</param>
	/// <param name="id">ID</param>
	/// <param name="bgmData">BGMデータ</param>
	private void ChangeBGM(float rate, string id, BgmData bgmData)
	{
		if (m_id != id)
		{
			m_intro.Audio.clip = bgmData.BgmIntro.BGM;
			m_loop.Audio.clip = bgmData.BgmLoop.BGM;
			m_intro.Audio.Play();
			m_loop.Audio.PlayDelayed(bgmData.BgmIntro.BGM.length);
			m_id = id;
		}
		m_intro.Rate = bgmData.BgmIntro.Rate * rate;
		m_loop.Rate = bgmData.BgmLoop.Rate * rate;
	}
}

VolumeChangerは自作で以前にこちらで作成したものです。

不要であればAudioSourceに変えてしまっても問題ないかと思います

まとめ

ゲームにイントロがあると非常に盛り上がりますがループとは非常に相性が悪いですがこのように実装すればきれいにまとめることができるかと思います

あとは、

  • 切り替え時のフェード
  • 各種音源の音量がバラバラの場合の調整
  • マスタボリュームコンフィグでの調整

と諸々まとまっています

これであればBGMをデータ化する手間がありますが、そこさえクリアすればゲームの

クォリティも1段階上がることでしょう

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT US
vuniformity誰でもない人
トレンドの行く末を見守っている
仮名を名乗るエンジニア

ゲーム開発は仕事であり趣味である
プログラムだけでなく多種多様なスキルを数多く持つ

ゲーム開発は
ソーシャルゲームを開発運用の経験アリ
ゲーム以外にも経験アリ
Webサービス保守開発等に携わる

ゲームプレイの主な戦場は
FGO
FEH
MTGA
マビノギ
ここでは主にunityroomで公開しているゲーム作り直しの軌跡を綴っていきます