🐙

【UE5/C++】MetaSoundで音楽を動的に生成する音ゲー開発

に公開

概要

https://historia.co.jp/ue5petitcon24
第24回ぷちコンに参加してみようと思い、
この機会に興味のあったMetaSoundと向き合ってみることにしました。

よくある3Dのランゲームにリズムアクションの要素を取り入れた音ゲーというざっくり企画で、
技術的にはMetaSoundによる動的なBGMの生成、
およびサウンド側からのイベントドリブンなゲームロジックの制御
みたいなことがやれたら良いなと思います。
作業しながらメモがてら記事を作成しているので、読みづらい部分はご容赦ください。

今回作ったゲーム

https://www.youtube.com/watch?v=d5c9K4ikIxo&ab_channel=改造姫
黄色い推奨ルートに沿ってキャラクターをマウスで移動させつつ、
左右のパンチを使い分けて敵のドローンを破壊していくランゲームになります。
敵を破壊し損ねると、攻撃されてしまいHPが減少し、HPが無くなるとゲーム終了です。

参考にさせていただいたもの

https://zenn.dev/posita33/books/ue5_metasound_createsound/viewer/chap_00_about
こちらを参考にMetaSoundでやれることをざっくりと把握させていただきました。

https://historia.co.jp/archives/37920/
MetaSoundSourceのTriggerなOutputを外部から検知する方法で参考にさせていただきました。

実装準備

MetaSound側でテンポや拍などを考慮した音楽的なイベントを管理し、
その情報をC++に伝えることによって、ゲームロジック側が音楽と同期できる仕組みを整えます。

MetaSoundSourceアセットの作成

まずはMetaSoundのアセットを作成してみないことには始まりません。
MS_DynamicBgmを以下のように作成しました。

BPM = 120のテンポ、4分音符のリズムで単純なサウンドを発声し、OnBeatをトリガーします。

OnBeatトリガーを検知してゲームロジックを動作させる

UMetaSoundOutputSubsystemというWorldSubsystemが用意されており、
これを介してMetaSoundSourceのOutputを監視することでイベントを受け取ることができます。
今回はAudioComponentを持つARunGameDynamicBgmProxyというアクタークラスを用意してみました。

if (UMetaSoundOutputSubsystem* Subsystem = GetWorld()->GetSubsystem<UMetaSoundOutputSubsystem>())
{
	FOnMetasoundOutputValueChanged OnBeatDelegate;
	OnBeatDelegate.BindDynamic(this, &ARunGameDynamicBgmProxy::OnBeat);
	// DynamicBgm: MetaSoundSourceがアサインされたAudioComponent
	Subsystem->WatchOutput(DynamicBgm, TEXT("OnBeat"), OnBeatDelegate);
}

MS_DynamicBgmに定義したOnBeatトリガーによって、
ARunGameDynamicBgmProxy::OnBeatが呼び出されるようになりました。
これにて、MetaSoundSourceのトリガーをC++側の処理に通知することができました。

補足ですが、MetaSoundアセットからBPMの情報を取得したい場合についても同様に
UMetaSoundOutputSubsystem::WatchOutput()で情報を受け取ることができます。
FOnMetasoundOutputValueChangedデリゲートには2つの引数が定義されていて、
第2引数の方から、FMetaSoundOutput::Get()で値を取得することができます。

ゲーム内のあらゆる要素がDynamicBgmとやり取りする

ここはゲーム側の設計の話なので本題とは逸れるんですが、
URunGameDynamicBgmManagerというWorldSubsystemを作成し、
それがURunGameDynamicBgmProxyをスポーン、管理する設計としました。
DynamicBgmと結びつくデリゲートなどはこのSubsystemが持ち、
ゲーム内のオブジェクトはこのSubsystemを通して音楽と同期することにします。

MetaSoundを用いた動的に生成されるBGMの実装

BGMを一本の波形データとして持つのではなく、
MetaSoundの機能として生成できるSin波などの基本的な波形をベースとして、
シンセサイザー的なものを自作し、それを鳴らすことでBGMとして成立するようにします。

ただしメロディを完全に動的に生成するのはちょっとハードルが高いので、
4小節単位の断片的なフレーズをMIDIデータとして手打ちで作成し、
それらをランダムに組み合わせてメロディを構成するような設計とします。
MIDIの再生にはHarmonixプラグインを利用しますが、UE5.6時点で実験的機能でした。

MS_DynamicBgm

このようになりました。

MSP_○○というのは、MetaSoundPatchアセットというもので、
MetaSoundSourceに組み込める自作ノードを作るためのアセットです。
MetaSoundSourceにはOnPlayとOnFinished、AudioのOutputという3つのピンが必須ですが、
MetaSoundPatchには必須ノードが無いため、スッキリと処理をまとめることができます。
これ自体はSoundアセットではないので、AudioComponentにアサインしたりはできません。
今回はこのMetaSoundPatchを利用して以下の機能を分割しました。

MSP_BeatManager

  • BGMのビートを管理する心臓部
  • サウンドには直接関与せずタイミングの管理のみを行う
  • MetronomeMIDIClockGeneratorノードでMIDIクロックを生成
    • 生成したMIDIクロックをMIDIトラックの再生、イベント発火等に利用
    • 音の再生とゲーム内ロジックがMIDIクロックに支配されるので原理的に音ズレが発生しない

MSP_RhythmTrack

  • 延々と単純なビートを刻むだけのリズム担当のトラック
  • MSP_BeatManagerの生成したイベントから波形ファイルを再生する
  • 余裕があればリズムの変化やフィルインなど入れたい(MIDI化するのが楽そう)

MSP_PhraseTrack

  • MIDIファイルを再生するメロディ担当のトラック
  • 出力はエンベロープの波形とMIDIノート番号に基づいた周波数
  • 音色は外部から与えて、エンベロープ波形と乗算する形で利用する
  • MSP_Oscilator_SuperSawが音色にあたりますが、音作りは本題ではないので割愛します
    • 楽しいから色々やって遊ぼう!

MSP_ChordTrack

  • MIDIファイルを再生するコード(和音)担当のトラック
  • MSP_PhraseTrackは和音の再生には対応していないので、こちらのトラックで対応する
  • 最大同時発音数をあらかじめ決めておいて、その数だけAudioのOutputが必要
  • MIDINoteTriggerに同時に複数ノートの情報が入力されても、1つしかトリガーされない
    • 今回は単音ごとに別チャンネルにノートを持つようにMIDIファイル側で対応
    • 最大同時発音数を4つとして、1~4チャンネルを愚直に処理
    • もっと良い解決方法があるかも

動的なBGMの生成

リズム、メロディ、コードという音楽の三大要素を生成する準備が(無理矢理)整いました。
用意したMetaSoundPatchたちを駆使して動的なBGMの生成に挑戦しましょう。
https://x.com/kaito1098/status/1949027374819148096
アルペジオ、メロディ、コード、ベースの4トラック構成としました。
それぞれ4小節のフレーズを8パターン作成し、ランダムに組み合わせながら再生していきます。
コードとベースは同じコード進行をなぞって欲しいので、ここだけフレーズの選択は共通で、
アルペジオとメロディは完全にランダムでフレーズが選択されるようになっています。
(本当はアルペジオもコードトーンをなぞるのが普通なんだと思いますが、細かいことは気にしない)

今回はスルーした課題点など

既に8×8×8=512パターンのフレーズがランダムに繰り返される構成となっており、
全てのパターンにおいて音楽的に破綻しないか確認するのは、なかなか大変な作業になりそうです。
この和音はマズいでしょ、みたいなアンチパターンを排除する機構が必要になると思います。

また、せっかくの動的生成BGMなので、
ゲームの進行に合わせて適切にBGMの展開が組み立てられる仕組みが欲しいですよね。
盛り上がるところで盛り上がり、落ち着いたところでは静かになる。
盛り上がる前には盛り上がりに向けて駆け上がるフレーズが構築される必要がありますし、
落ち着いたところではトラックの編成ごとガラッと変えたくなることもあると思います。
このあたりはゲームの仕様との親和性も大事なので、企画の段階から練っておくと良いですね。

音楽的にはミックス周りにも課題が多いです。
トラックごともそうですし、マスタートラックのエフェクト処理も今のままでは扱いづらいですし、何よりモノラルミックスにしか対応できていません。
またシステムを組むエンジニアがサウンドのプロであるケースは稀だと思うので、
サウンド担当の人と円滑に作業ができるよう、ワークフローの検討も進めなきゃいけません。
MetaSoundPatchアセットの分割の仕方も、その視点から見直した方が良いと思います。
DAWでの作業に慣れたサウンドさんが触れる環境を作るのは、かなりハードルが高いと思います。

何でもイチから仕様を考えられる面白さは感じますが、
そこまでこだわっていては開発が終わらなくなりそうなので、今回はグッと堪えてスルーします。

そういえばゲームを作っていたんだった

すっかり忘れていました。
当初の予定通り、リズムアクションの要素を取り入れたランゲームとして仕上げていきましょう。
細かい実装の話は書いてもしょうがないので、今回の開発ならではの部分を抜粋して触れてみます。

キャラクターがBGMと完全に同期しながら無限に前方へと走り続ける

単純な移動しか行われないゲームなので、ひとまずMovementComponentは無効化しています。
BGMと完全に同期した位置を走り続ける必要があるので、基本的には1小節分の道の長さと、
現在のBPMから計算した速度で前進し続けるようにしています。
が、意図しない処理落ちや計算誤差の積み重ねによるズレが生じる可能性が捨てきれないため、
1小節ごとに床を再配置する際、プレイヤーの位置も床の始点に強制的に合わせるようにしました。
想定よりズレが大きい場合は妙な挙動になってしまう可能性があるので、
本来はもっと細かい単位で同期を取る方が良いと思います。

なお、アニメーションの速度は厳密には移動速度ではなく、
これまたURunGameDynamicBgmManagerから受け取ったBPMをもとにPlayRateを計算しています。

敵やスコアアイテムなどのオブジェクトをBGMに合わせて配置する

スコアアイテムはともかくとして、敵の配置は音ゲー的には重要です。
完全にランダムで置いただけでは演奏してる感が無く、面白味に欠けます。
とはいえ、MIDIデータを解析してノートのタイミングを基準に敵を配置していくというのも、
BGM側にゲームとしての面白さを依存する形となり微妙な気がします。
速くてカッコイイフレーズを難しくなりすぎるのでダメですと切り捨てたり、
切なくて盛り上がるフレーズを簡単すぎるのでダメですと跳ねのけるのはもったいないですね。

というわけで譜面データはMIDIファイルとは別に用意しつつ、
譜面データを元にノートオブジェクトを配置することにしました。
譜面データはMIDIファイルと紐づけているので、
譜面作者が音楽に合わせて面白い譜面を作るという分業体制が生まれます。みんな幸せ!

なお、譜面データはデータテーブルに登録された文字列データとなります。
譜面作者の皆さまは恐怖に震えるがよいです。(さすがにエディタは作らなかったよ)

最後に

ぷちコン参加が目的ということで、駆け足気味の検証・実装でした。
MetaSound、かなり自由度が高くやれることが沢山ありそうな手ごたえでしたが、
現状自前で実装しなきゃいけない部分が多く、高度な要件を満たすにはハードルが高そうと感じました。

その一方で、
HarmonixプラグインでのMIDI再生やサンプラー機能など、将来性が楽しみな雰囲気も感じました。
その方向で発展していくと、もはやちょっとしたDAWみたいなシステムになりそうですが、
そうなってくるとサウンド分野に強い個人開発勢にも色々実験できて面白そうだなと思います。
VSTプラグインとか挿せるようになったら...夢が広がりますね!!!!

Discussion