はじめに
ADXアンバサダーのニム式です。今回はADX for Unityを使って、ゲーム中の展開に応じてBGMがシームレスに変化していく手法「インタラクティブミュージック」をUnityに導入する手順を紹介します。これによりゲームプレイと音楽を連動させ、盛り上がりなどの演出をより強化することができます。
本記事では、現在の状態に応じてループ楽曲の曲調とテンションを変化させていくインタラクティブミュージックの実装方法を紹介します。
この記事は「ADX2 for UE4で変化するループBGMを再生する(UE4編)」のUnity版です。
元記事作者のSigさん ( https://qiita.com/SigRem )の許可を得て作成しています。
動作確認環境
Windows 10 Home 21H2
Unity 2021.3.15f1
ADX LE Unity SDK 3.07.02
CRI ADX LE Tools for Windows 3.47.01
前提記事
CRI ADX導入方法や基本的な操作については以下の記事を参照して下さい。
Unityのサウンド機能をADXで強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377
やること
- AtomCraftでブロック再生とAisacを設定する
- 曲のフレーズを変化させる「ブロック再生」機能
- 曲の盛り上がりを変化させる「Aisac Control」機能
- Unityで上記の設定をコントロールする実装をする
- トリガー
- トリガーの主体となるプレイヤー
- トリガーのデータで曲をコントロールするマネージャー
実装
AtomCraftでブロック再生とAisacを追加する
まずは、Unityとは別のツール「AtomCraft」で再生する音データの作成を行います。ADX for Unityを導入した環境においては、ゲーム内で再生する音の設定をこのAtomCraftで行っていきます。
AtomCraftでの設定手順については、元の記事(UE4向け記事)のAtomCraft編と同じになりますので、この記事では省略します。
ADX2 for UE4で変化するループBGMを再生する(AtomCraft編)
https://qiita.com/SigRem/items/a6ccd57a2881e8a43905
本記事の手順を試す場合は、まず元の記事の該当項目を進めてから本記事を進めてください。
UE4版との違いとして1点、「Atomキューシートバイナリのビルド」画面で、一番左下のオプション「UnityAssets出力」にチェックを入れるのを忘れないようにして下さい。
またUnityへのインポート手順は以下の記事、「Unity側のセットアップ」の項を参照してください。
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377#unity側のセットアップ
プレイヤーとレベルを作成する
Unityへのインポートが終わったら、まずはプレイヤーオブジェクトを作成します。Capsuleオブジェクトを作成、tagをPlayerにし、コライダーをアタッチしisTriggerをtrueにします。
次に音の状態をコントロールするためのレベルを作成していきます。
音データは4つのブロック、3つのテンションを掛け合わせた計12種類の状態が存在するため、それぞれの状態に対応する12個のエリアを用意します。そこにプレイヤーが入った時、再生している音データをエリアに対応した状態に変化させる、というシーンを作っていきます。
各エリアには、コライダー、IMBlockChangeTriggerクラスをアタッチします。画像ではテスト時分かりやすいようにPlaneとテキストをアタッチしています。
IMBlockChangeTriggerクラスは前述の12種の状態を表現できる変数(phrase、tension)を持っており、プレイヤーがコライダー内に侵入した時マネージャーへ通知する役割があります。Initializeメソッドについては後述します。
public class IMBlockChangeTrigger : MonoBehaviour
{
[SerializeField] private int tension;
[SerializeField] private int phrase;
[SerializeField] private TextMeshPro tmp_Tension;
[SerializeField] private TextMeshPro tmp_Phrase;
private IMBlockChangeController iMBlockController;
void Awake()
{
iMBlockController = GetComponentInParent<IMBlockChangeController>();
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player")) iMBlockController.SetBlockData(tension, phrase);
}
public void Initialize(int _tension, int _phrase)
{
tension = _tension;
phrase = _phrase;
tmp_Tension.text = _tension.ToString();
tmp_Phrase.text = _phrase.ToString();
transform.position = new Vector3(_tension * 10 - 10, 0, - _phrase * 10 + 15);
}
}
12のエリアを統括する親オブジェクトにIMBlockChangeControllerをアタッチします。
public class IMBlockChangeController : MonoBehaviour
{
[SerializeField] private int tension;
[SerializeField] private int aisac_0;
[SerializeField] private int aisac_0_current;
[SerializeField] private int aisac_1_current;
[SerializeField] private int aisac_1;
[SerializeField] private int phrase;
[SerializeField] private string phraseString;
[SerializeField] private CriAtomSource bgmCriAtomSource;
private CriAtomExPlayback playback;
private bool trigger;
void Start()
{
bgmCriAtomSource.SetAisacControl("AisacControl_00", 0f);
bgmCriAtomSource.SetAisacControl("AisacControl_01", 0f);
playback = bgmCriAtomSource.Play();
}
void Update()
{
playback.SetNextBlockIndex(phrase);
if (trigger == false) return;
switch (tension)
{
case 0:
aisac_0 = 0;
aisac_1 = 0;
break;
case 1:
aisac_0 = 1;
aisac_1 = 0;
break;
case 2:
aisac_0 = 1;
aisac_1 = 1;
break;
default:
break;
}
DOTween.KillAll();
DOTween.To(() => (float)aisac_0_current,
(x) =>
{
bgmCriAtomSource.SetAisacControl("AisacControl_00", x);
},
aisac_0,
3f).OnComplete(() => { aisac_0_current = aisac_0; });
DOTween.To(() => (float)aisac_1_current,
(x) =>
{
bgmCriAtomSource.SetAisacControl("AisacControl_01", x);
},
aisac_1,
3f).OnComplete(() => { aisac_1_current = aisac_1; });
trigger = false;
}
public void SetBlockData(int tension, int phrase)
{
this.tension = tension;
this.phrase = phrase;
trigger = true;
}
private void OnValidate()
{
var collection = GetComponentsInChildren<IMBlockChangeTrigger>();
for (int i = 0; i < collection.Length; i++)
{
var item = collection[i];
item.Initialize(i % 3, i % 4);
}
}
}
これには音の再生と、IMBlockChangeTriggerから受け取ったデータによって再生中の音をコントロールする機能があります。
音データは、BlockIndexとAisacによるコントロールができる設定になっているので、phrase・tension変数で設定できるように変換などを行います。
ブロックの再生位置の制御については、SetNextBlockIndex()に0~3の値を渡せばそのまま対応するブロックを再生できるため、各エリアのphrase変数は0~3を設定します。
テンションの制御については、ロー・ミドル・ハイの3種を制御したいため、エリアのtension変数は0,1,2の3種類にします。しかし、AtomCraft編で作った音データは2つの楽器パートセットのオンオフをコントロールすること、つまり2つのAisacでテンションを制御しているため、以下のような変換を行います。
Aisac1 | Aisac2 | ||
---|---|---|---|
tension | AisacControl_00 | AisacControl_01 | |
ロー | 0 | 0 | 0 |
ミドル | 1 | 1 | 0 |
ハイ | 2 | 1 | 1 |
OnValidate、Initializeメソッドについて
同じオブジェクトが少しずつ違うデータや初期位置を持つ場合、手作業で設定していくのは非常に手間です。今回のように適用データに規則性がある場合は、スクリプトで自動で処理できるようにすれば、手間を減らしかつ間違いも減ります。
OnValidateは制約はあるもののゲーム編集中(再生していない状態)に実行できるメソッドです。自身の変数に変化があった場合や、スクリプト自身の有効・無効の変化時に実行されるため、任意のタイミングで実行する場合は主に後者を利用します。
サンプルでは、準備としてエリアオブジェクトを1つ作成した後に手動で11個コピーしておき、前述のOnValidateを実行すると、すべてのエリアオブジェクトのInitializeメソッドが実行され初期化が行われるようになっています。
テストする
ここまで実装ができればテストをします。以下のように鳴ればOKです。
プレイヤーオブジェクトを左右に移動させると、次に再生されるフレーズが変わります。
上に1段移動するとパッド、ベース、ドラムが追加されます。最上段ではさらに、リードと追加のドラムが鳴り出します。
まとめ
現在の状態に応じてループ楽曲の曲調、テンションを変化させていく「インタラクティブミュージック」を実装する方法を紹介しました。
ブロック再生に加えAisacを利用すると、制御コードの実装自体はさほど難しくないですがかなりバリエーションに富んだインタラクティブミュージックが可能になります。