大福未来研究所

大福フューチャーラボのなんか色々なアレ。プログラミング(C++/DirectX/Unity/UE4あたりやりたい)とか音楽とかの。

【UE4】BGMのテンポに合わせた命令実行機能を作る

この記事は、裏Unreal Engine 4 (UE4) Advent Calendar 2016への参加14日目の記事です。
qiita.com
昨日はyoshikataさんによる
UE4.14・AndroidでVRの実機プレイまでの解説 - Qiitaでした。


曲に合わせて演出したい

例えばゲームで敵が出現するタイミングだったり、演出が起きるタイミングだったり、「ステージ開始から何秒でなにをどうしてこうなって……」と、
画面内でシーケンシャルな動作・演出を行いたいときというのは往々にしてあると思います。
そういう時、単にゲーム内でのことを考えるなら時間で考えれば良いのですが、
これが楽曲のPVを作ったり、音楽に合わせたゲーム内での演出だったらどうでしょう。
何秒で、というよりは「何小節目の何拍目に何をどうしてこうなって」という考え方のほうが、捗りやすいと思います。
ですので、今回の記事ではその仕組みを実装していきます。

(注意:この記事で実装する方法では、楽曲のテンポが一定であることが必要となります。)

また、今回は文字列をいじることも多いのですが、alweiさんの
unrealengine.hatenablog.com
こちらの記事がとても参考になりました。この場を借りて御礼申し上げます。

必要な機能を考える

さて、楽曲にそって処理を行う仕組みを実装するにあたって、必要な機能を考えてみます。

スクリプトを書き込んだCSVをインポートして、データテーブルとして読み出す
・テンポと小節数、拍数を時間に変換する
・構造体を作り、時間と処理を紐付けして配列にして格納して時間に応じて実行

という感じになると思うのですが、ここで一つ大事になるのは

楽曲に合わせた演出を行う場合、どれだけ細かい音符や珍しい分解能の音符でも対応できる必要がある、ということです。
たとえば楽曲の”キメ”の部分は6分音符になるような楽曲だと、一番細かくても16分音符でしか表現できない仕組みだった場合、
どうしても近似値となる音符で代用する必要が出てきます。その場合、音楽に演出や動きが微妙に合っていないという状況が発生し、
せっかくのテンポに合わせた命令実行という気持ちよさを生み出すための機能が台無しになってしまうことになります。

そのため、今回参考にしたのはBMS(Be-Music Script)の譜面スクリプトの考え方です。
hitkey.nekokan.dyndns.info
これは、PC上で動作する音楽ゲームBMS」における譜面スクリプトのフォーマットで、説明するととても長くなるのでお時間のあるときにでもリンク先を読んでみると良いと思います。

このフォーマットの譜面部分は1行で1小節を表記し、

「#WAV01:01」

とある場合、1小節目の頭に音符01が一つ置かれる形となります。(全音符)

「#WAV01:01010101」

とある場合、1小節目の頭から4分音符の間隔ごとに音符01が4つ置かれる形となります。
また、休符は00で表現します。

このように、記述した音符・休符の数に応じてプログラム側で自動的に小節の分割数を決めることが出来るため、
とても自由度の高いスクリプト記述が行えます。
今回の記事で作るスクリプトのシーケンス部分は、このシステムを取り入れています。


スクリプトを作る

まず最初に、構造体を作りましょう。
f:id:dfk_ohnuma:20161214013924p:plain
中身はこれだけです。
そして、これに対応したCSVファイルを作り、ちょっとだけ記述してみました。

f:id:dfk_ohnuma:20161214022435p:plain
私は今回、楽曲ではなくムービーそのものを流すために作ってみたので、最初が「Movie」と書かれていますが、
音楽のために作る場合はSoundとかBGMとかで良いと思います。

今回のスクリプトの仕様としては、

"MovieX":X番目のムービーを指定する(今回は1つしか使わないので0番目だけしか指定してませんが、複数使いたい時は増やせます。)
"BPM":テンポを指定する。
"seqX:Y":X小節目の意味。右側には処理のIDをBMS譜面のような形式で書き込む。Yは同時に2つ以上の処理を行いたい場合などに、seq1:0とseq1:1のように、Yの数字を増やして使う。

となっています。

3小節目は02と03が8分音符のタイミングで交互に、4小節目は16分音符のタイミングで交互に繰り返されているのがわかりますね。
1曲まるまる作るならもっと長いスクリプトファイルになると思います。

スクリプトのために下準備をする

次に、UE4側で下準備をします。
まずはムービー(orサウンド)をスクリプトから指定できるように、文字列に紐付けしたデータテーブルを作っておきます。
f:id:dfk_ohnuma:20161214021818p:plain

そして、スクリプトを読み込んだときに、どの時間にどの処理を行うのかを受け取らなければいけないので、処理用の構造体を作ります。
f:id:dfk_ohnuma:20161214021951p:plain
Time:スクリプトを読み込んで計算された、メディア再生からの経過時間
OperationNumber:処理内容ID

スクリプトを読み込む部分を作る

さて、ここから本番です。まずは読み込むためのBPを作ります。

ちなみに私の作ったBPでは、こんな感じの変数リストになってました。
f:id:dfk_ohnuma:20161214024141p:plain

Media:メディアプレイヤーを予め入れておく配列。たぶんサウンドでやるならAudioComponentか何かになってる
BPM:テンポを入れておく変数
Operation:さっき作った処理用の構造体
forSort:ソートに使ってました
MediaStartTime:再生開始時間を入れておく変数
SequenceStarted:処理が始まっているかどうか

文字列からムービーやらテンポやら処理やら、どれについて書かれた行なのかを判断する必要があります。
f:id:dfk_ohnuma:20161214021552p:plain
うーん、かなり長いですね。ということなので、1つずつ見ていきましょう。


まずはムービーのロード部分です。
f:id:dfk_ohnuma:20161214022932p:plain
f:id:dfk_ohnuma:20161214022848p:plain
データテーブル内のMovieXを検索し、そこに書かれている文字列に対応したメディアをメディアプレイヤーへセットします。
これを楽曲で行う場合は、おそらくSoundXを検索した後、そこに書かれている文字列に対応したサウンドをAudioComponentへセットする形になると思います。


次にBPM設定の部分です。
f:id:dfk_ohnuma:20161214023232p:plain
これは普通ですね。


ではシーケンス部分の読み込み処理を見ていきます。
f:id:dfk_ohnuma:20161214024326p:plain
全体図はこんな感じです。
1行ずつ読み込んで文字列の分解を行い、その結果をOperation配列へ登録。
それが全部終わったら、Timeが短い順にソートする。
という流れになっています。

行ごとの読み込み

f:id:dfk_ohnuma:20161214025509p:plain
99999まで続くForLoopWithBreakが2つありますが、"seqX:Y"におけるXが左側、Yが右側と対応しています。
とはいえいくらなんでも繰り返し回数が多いと思いますので、適当に削ってやっていただいて大丈夫です。
両方から持ってきた数字と"seq"と":"をAppendノードで結合して、データテーブルから値を引っ張ってきます。

文字列の分解

値を引っ張ってきたら、それを分解します。
f:id:dfk_ohnuma:20161214030130p:plain
まずは「IsNumeric」ノードを使って、値となっている文字列が全部数字であるかどうかを確かめます。
次に、文字列の全長を2で割った回数だけForLoopを行い、
2文字ずつ「GetSubstring」ノードで引っ張ってきます。引っ張ってきた2文字をIntにした時、0(="00"、つまり休符)ではない場合は、そのままオペレーション登録へ進みます。

オペレーション登録

処理IDとタイミングが取り出せる状況になったので、タイミングを時間に変換して配列へ登録します。
f:id:dfk_ohnuma:20161214030704p:plain


(60÷BPM)*4=1小節分の長さ
文字列の長さ÷2=小節内の分割単位
1小節の長さ÷小節内の分割単位=1分割単位分の長さ
行ごとの読み込みの左側から取り出せるIndex=何小節目か(A)
文字列分解のForLoopから取り出せるIndex=小節の頭から分割単位いくつ分ズレているか(B)


つまり、

1小節分の長さ×A+1分割単位分の長さ×B=その処理を行うべき経過時間
ということになります。

配列のソート

BPを使った配列のソートについては、こちらのサイト記事を参考にさせていただきました。
ゲームプログラムメモ — Blueprintで配列ソート #UE4Study

処理用の配列を、経過時間が短い順でソートします。
f:id:dfk_ohnuma:20161214031838p:plain
ここまででようやく準備が完了しました。
あとはこれを実行するときの部分になります。

実行部分

まずはBeginPlayに先程作ったスクリプトロードの関数を入れます。
f:id:dfk_ohnuma:20161214032100p:plain


処理を開始する時は、
f:id:dfk_ohnuma:20161214032248p:plain
このようにして、開始時間を記録すると同時にシーケンス処理開始のフラグを立てます。

そして、Tick処理がこちらです。
f:id:dfk_ohnuma:20161214032637p:plain
配列の最初の要素のTimeを確認し、経過時間がそれに到達していたらオペレーション実行のイベントを呼びます。
その後、不要になったそのインデックスを配列から削除します。


オペレーション実行のイベントでは、
f:id:dfk_ohnuma:20161214032747p:plain
このようにSwitch分岐でIDごとに処理を分けます。好きな処理を(99種類までなら)好きなだけ定義することが出来ます。
もちろん、処理IDが多くなればなるほど見た目が複雑になりがちなので、その時は以前の記事のようにマクロを使うなどして可読性を高める必要があると思います。

まとめ

スクリプトの分解は面倒だけど、理屈がわかれば面白い
BMSスクリプトを考えた人は偉大
・これを上手く使えば楽曲のPVを作ったりすることも出来るのでは……?

というわけで、これでこの記事はおしまいです。
ここまで読んで頂き、ありがとうございました。



明日はmonsho1977さんの「ComputeShaderを使ったポストプロセス追加手法」です。