ITGギミック制作記5: Chase Me D23の落ちサビを作る (連続的なオプション変化)
一通りStepManiaもしくはITGの譜面が作れるようになって、ちょっとギミックを使って演出を強化したりプレイヤーの邪魔をしたりしたいな、という方向けにいくつか記事を書いていこうと思います。第5回です。
今回は、前回作成したChase Meもどきをもう少し発展させて、連続的にオプションを変化させてChase Meの落ちサビもどきを作ります。
今回はこの動画の落ちサビ (1:43~) にある、譜面逆流+フェイントを作成します。今思えば、このギミックを作るためだけにluaの勉強を始めたはずなのですが思えば変なところにたどり着いてしまった気がします。
さてPIUではこの手の譜面逆流は頻出です。これは、一瞬だけハイスピを0かそれに近い値にして、その直後からハイスピをゆっくり元の値にまで戻すことで作られています。こういう挙動の作り方としては、例えば
こういう方法があります。ハイスピを0にする命令を出して0.02秒後に元に戻す命令を出すパターンです。Update形式を使っている場合にも、下のように書くよりは、上のようにまとめてしまった方が良いです。
(↓あまり良くない例: 動かないことが多い)
これはUpdate形式における分解能の問題です。どうせ0.02秒ごとにしか更新しないのなら、上のように書いてしまった方が楽です。
ところで、「オプション変更命令を出した次のフレームに命令を出す」というあまりにも使い道の限定されたコマンドがあります。それがDelayedGameCommandです。
DelayedGameCommandを使った例です。この例では、ノートを100%ステルスにした次フレームから、少しだけ時間をかけて徐々にステルスを解除します。これを実行すると、矢印が一瞬白く光るような演出になり、例えば譜面ワープ後の矢印出現を少し自然にできたりします。
これを使えばPIU的な譜面逆流も簡単に書けそうです。上の例をまとめるとこうなります。
これはDelayedGameCommandの致命的な弱点なのですが、プレイヤーごとに命令を出すことができません。なのでこの書き方をすると、バーサスプレイで大変なことになります。どちらかといえばダブル向けの命令です。
ただこれを使えばconflictのビームが簡単に書けたりします。こんな感じです。
Update形式での分解能に引っかからないので、ホールドをかなり短くしても反映されると思います。
話が逸れました。ともあれ今回再現する譜面を改めて見てみますと、
次の譜面の1小節前にワープ→28拍停止→BPM200で再開
という挙動になっています。これはなかなか厄介です。なぜ厄介なのか説明します。
・次の譜面の1小節前にワープ
これは第1回に説明した方法で簡単に作れます。なので大した問題ではないのですが……
・28拍停止
ここが大問題です。なぜなら、この間ハイスピを変えまくらないといけないのに拍数がずっと一緒なのでUpdateがまるで働かないからです。
・BPM200で再開
ここもかなりの問題です。ハイスピをゆっくり上げている間に再開すると矢印が滑らかに動かずカクカクとした動きになるためです。
とりあえず譜面を書きましょう。前から使ってる奴のBPMを200に上げて、ついでに末尾に譜面を追加します。
かなり縦長になってしまいましたが……ワープ(赤色)の直後、6小節目頭に8.4秒の停止が入っています。これでChase Me感満載の譜面になりました。
次にxmlを弄ります。
6小節目頭にChaseMeを追加しまして、
とりあえず2Pの人のことは考えないこととしてDelayedGameCommandでそれっぽく書いてみます。
適当に譜面逆流を実装した例 pic.twitter.com/gypuI4BjXJ
— paraphrohn (@paraphrohn) July 5, 2019
うーん、何とも問題が多いですね。フェイントとかがないのは書いてないので当然ですが、やっぱり加速する瞬間に矢印が急に動き出すことと、ハイスピが完全に戻り切っておらず譜面が始まってからも微妙にスクロールが遅いことが現状の課題です。あとやっぱり2P側がどうしようもないというのも気になります。一つずつ解決していきましょう。
・フェイントを書く
Chase Meの逆流地帯8.4秒の内訳は(おそらく)以下の通りです。
譜面逆流(遅) 4.05秒
譜面逆流(速) 0.3秒
フェイント 0.15秒
譜面逆流(速) 0.45秒
譜面逆流(遅) 3.45秒
というわけで、これをイノセントに書き下すならば
こんな感じになりそうです。残念ながらこの記法では全く思い通りに綺麗に動きません。
少し面倒な話になりますが、self:sleep()はself:関係のコマンドを休止する命令であって、それ以外の命令、例えばGAMESTATE:や各種演算、条件分岐などの実行には何ら影響を及ぼしません。これがどういうことかと言えば、上の命令が実行されると
1. ハイスピを0xに変更
2. ハイスピを元の1/2に変更
3. ハイスピを元のハイスピに変更
の4つの命令を1フレーム内に行い、結果としてハイスピは元のハイスピに変更=変化しないということになります。
その後、1フレーム置いてDelayedGameCommandが発動してハイスピがゆっくり1/4になります。
さらにその後にself:sleepが発動して、虚無を待ち続けることになります。
というわけでまずはここを直す必要があります。簡単に言えばself:系でハイスピを変更させてやればいいので……queuecommandを使って、別のコマンド内でハイスピを変えてやることが考えられます。
こういう形で、それぞれのChaseMe1Command、ChaseMe2Command・・・でGAMESTATE:ApplyGameCommandを使ってやります。これで順番にオプションが動くようになりました。
各コマンド内の細かな数値、設定については最後にまとめて書きます。
・加速する瞬間に矢印が急に動き出す
これの対処法は実は割と簡単です。加速する瞬間にはハイスピを元の半分くらいにしておいて、加速した瞬間からハイスピの変更速度を大きくしながら元のハイスピに戻してやれば比較的スムーズにできます。
BPM変化によるスクロール速度変化をハイスピ変更速度の差で打ち消してやるイメージです。
・譜面開始時にハイスピが戻り切っていない
上の手法を使えば解決です。
・2P側がどうしようもない
せっかく紹介したところですが、DelayedGameCommandを諦めるしかありません。「フェイントを書く」を参考にすれば、例えばこういう書き方が考えられます。
ChaseMe1Commandで、ゆっくりハイスピを上げていく形にします。0.01秒しか差がないので、まあ1フレームみたいなものです。
というわけで、ここまでの対策をまとめて、細かく調整したものを下に載せます。
かなり気持ち悪いですが、これでそれっぽく動きました。
Chase Meっぽく調整した例 pic.twitter.com/uyeK3zjwzz
— paraphrohn (@paraphrohn) July 5, 2019
ところで上の動画はハイスピを3.0xにして撮った動画です。これを試しに2.0xにしてみます。
Chase Meっぽく調整した奴をハイスピ変えて再生したら大変ダサくなった例 pic.twitter.com/Dua89LL6yq
— paraphrohn (@paraphrohn) July 5, 2019
うーんダサい。あまりにもダサいです。これがどうして起こるのかChaseMe1Commandを例にとって説明してみますと
・ハイスピが3.0の場合
3.0 * 0.075 = 0.225から、3.0 * 0.25 = 0.75まで、ハイスピ差0.525を一定の速度(*0.1)で変化
・ハイスピが2.0の場合
2.0 * 0.075 = 0.150から、2.0 * 0.25 = 0.5まで、ハイスピ差0.35を一定の速度(*0.1)で変化
となり、かかる時間に差があることが分かります*1。なので、0.525変化するには4.04秒以上かかっていたハイスピ変更が、0.35だけ変化するときには4.04秒かからなかったためにダサい速度変化が生まれてしまったというわけです。
これを解決するには、ハイスピの変更速度もハイスピ依存にしてしまうのが一番手っ取り早いです。つまりこういうことです。
かなり書くのがめんどくさくなってきましたが、効果はてきめんで、下のようにバーサスプレイでそれぞれ違うハイスピを選んだとしてもきちんと動きます。
変更速度をハイスピ依存に修正した最終版 pic.twitter.com/KBIuGrHoCZ
— paraphrohn (@paraphrohn) July 5, 2019
というわけで今回の教訓です。
・DelayedGameCommandは1P2Pを指定できない
・self:sleep()とGAMESTATE:は組み合わせられない
・ハイスピ変更時の変更速度はハイスピ依存にするとズレない
これだけ覚えておけば、少なくともオプション変更関係のギミックで困ることは無いと思います。あとは発想次第でどんなものでも作れるかと。
ここまで5回+1回にわたって、ワープから始まってFG、ハイスピ固定、変更とある程度のギミック(具体的にはDoppelgangerを除くフメンタイン2019程度のギミック)が作れるくらいのxmlの書き方について説明してきました。
ここから先は作りたいギミックに応じて、以下のサイトで必要な情報だけ抜き取ってくるのが向いているかと思います。
例えば、現在の残りゲージに応じた演出を行うことを考えます。
上ページを「Life」で検索すると
こういったものが見つかります*2。このブロックの頭を見るとYou can get an instance of PlayerStageStats by using StageStats.GetPlayerStageStats().と書いてあります。そこでこのGetPlayerStageStatsをクリックしてみると……
ここに飛ばされます。見るとYou can get an instance of StageStats with the StatsManager class.とありまして……すぐ下にStatsManagerの欄があります。
今回は「現在プレイ中の曲におけるステータス」を参照するのでGetCurStageStatsが使えますかね……上側を見ればSTATSMAN singletonを使えって書いてあります。そうすると、ライフの取得は次のようになります。
STATSMAN:GetCurStageStats():GetPlayerStageStats(0):GetCurrentLife()
長い……ですがこれでちゃんと取得できます。()の中はvoidと書いてあるところは空白で、そうでないところは指示に従って書いていきます。
というわけでライフが減れば減るほどハイスピが下がるxmlを書いてみましょう。
今回は他の動作をしないことを前提に、Updateの中身にハイスピ変更を書き込みました。ちなみに致命的な欠点として1ノートも踏んでいない場合にはlifeは0として扱われるため最初の1歩までハイスピ0になるのですが、まあその辺はfgを置くタイミングを適当に変えれば何とかなると思います。
とにかく、ここまで覚えておけば調べものと発想、あとはifとかforとかを適切に使えるだけでまあいろいろ作れるということです。
本シリーズは今回で終了になりますが、もし万が一需要があれば、今までに動画を上げてきたギミック譜面についてxmlの中身と解説を書いていこうと思います。見てみたい奴があったら@paraphrohnまでよろしくお願いいたします。それではここまで長らくおつきあいくださりありがとうございました。