XaiJu
nalulululuna
nalulululuna

fanbox


Beat Saber mod 制作日記

今回は、ビートセイバーの動作を変えるmodを作るとき、わたしがどのような作業を行っているのか書いてみます。


以前のmod作成解説では、シンプルに丸写しでmodが作れるように解説を書きましたが、「こう書けば動作が変わる」の「こう書く」の部分をどうやって見つけるのか、については触れていませんでした。今回の記事では、その部分について書いていこうと思います。

dnSpy

今回はビートセイバーの中身を見るため下記のソフトを使います。

https://github.com/dnSpy/dnSpy/releases/latest


詳しい使い方は解説しませんが、dnSpy-net-win64.zipを展開して、中のdnSpy.exeを起動、メニューから File → Open... で

C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\Main.dll

を開くと、ビートセイバー内のコードを検索できる状態になります。


たとえばRawScoreWithoutMultiplierで検索するとこんな感じ。

float num = 1f - Mathf.Clamp01(cutDistanceToCenter / 0.3f);

cutDistanceRawScore = Mathf.RoundToInt(15f * num);

ノーツを切ったときの距離スコア(0-15)が、上記のコード(計算式)になっている、ということがわかります。

NalulunaFlyingScore

NalulunaFlyingScore、ビートセイバーのノーツカットスコアに色を付けたり、昔のFF風のダメージ表示風に表示したりするわたしのmodですが、ビートセイバーの1.13.4の仕様変更で、数字の跳ねる挙動を変えざるを得なくなってました。


具体的に言うと、以前のバージョンでは、跳ねる数字が各桁ごとに時間差で飛んでいたのが、今のバージョンでは全桁まとまって飛ぶようになっています(FF4,5からFF6の飛び方に変わってます)。これは、1.13.4以降、時間差で飛ばすと、なぜか表示されるスコアがおかしくなってしまう問題が出たためです。


これを直すため、そのうち一から作り直そうと思っているのですが、今回は、このmodを作る過程の一部をご紹介します。


昔の挙動

youtube post: jC9hXpYT0kQ


今の挙動

youtube post: uhpdZcPx1Nc

プロジェクト作成

よく使う機能をまとめた自分用のテンプレートを使ってプロジェクトを作成します。一から書くと面倒な設定画面や、どのmodにも必要になりそうな定型処理が既に入ったところからスタートできるようにしてます。

ノーツカットスコアの処理

どういった処理で、ビートセイバーがノーツカットスコアを出していたのかうろ覚えなので、そのあたりを確認するためdnSpyを起動します。適当に名前で当たりをつけて検索しましょう。"cutscore"で検索するとNoteCutScoreSpawnerというクラスを見つけました。これっぽいですね。


すこし思い出してきました。FlyingScoreEffectがカットスコアの文字や色などの制御を、(その親クラスの)FlyingObjectEffectがカットスコアの移動の制御をしています。

カットスコアを時間差で3回出す

どこから手を付けるか悩みますが、まず、今のバージョンで変わってしまっている挙動を元に戻すのが目的なので、それを実現できるか実験してみます。通常、カットスコアが1回出るところを、時間差で(同じカットスコアを)3回出るように変えたいと思います。


NoteCutScoreSpawner.HandleNoteWasCutにパッチを当てて、カットスコアを出す処理の後に、さらに時間差で2回カットスコアを出すようにしてみました。これで一回テストします。


ああ、スクリーンショットがうまくとれません…。もっと表示間隔を短くすべきでした。消えかけていますが、最初に飛んだカットスコアが18なのに、次に飛んだカットスコアは4になっています。ここには映っていませんが、3つ目のカットスコアも4でした。やはり時間差で飛ばすと、スコアの数字がおかしくなってしまうようです。

原因を調べる (カットスコアの数字が変)

何が起こっているかわからないので、各関数にログを仕込んで実行順を調べます。


すると…

----

InitAndPresent

SpawnFlyingScore:1

HandleSaberSwingRatingCounterDidChange

HandleSaberSwingRatingCounterDidChange

...

HandleSaberSwingRatingCounterDidChange

HandleSaberSwingRatingCounterDidFinish

InitAndPresent

SpawnFlyingScore:2

InitAndPresent

SpawnFlyingScore:3

----

2つ目のカットスコアを出す前に、SaberSwingRatingCounter(セイバーのスイング角度とスコアを管理する一時オブジェクト)が終了しているようです。


さらにSaberSwingRatingCounterの処理を追っていくと、このオブジェクトはノーツを切った瞬間(GameNoteController.HandleCut)に生成され、それから約0.4秒後に解放(Finish)されるような処理になっています。


dnSpyでカットスコアの処理を確認すると、SaberSwingRatingCounterがない場合、角度点は0点扱いになるようですね。2つ目と3つ目の点が少ないのはこのせいでしょうか。

解決法を考える (カットスコアの数字が変)

さて、考えられる解決法の1つは、SaberSwingRatingCounterが3つ目のカットスコアが消えるまで解放されないようにする(ただし、スイング角度の計算受け付け時間は増えないように)。


もう1つは、2つ目と3つ目のカットスコア表示処理を変えて、SaberSwingRatingCounterがなくなっていても正常な値が出るようにする、が考えられます。どっちにしましょう。


とりあえず、SaberSwingRatingCounterの解放を遅らせてみました。これでテストしてみると…、あ、あれ?これでもダメ…。


dnSpyでカットスコアを出している部分を精査すると、どうも初回のスコア計算では、中心カットスコア(0-15)が足されていないみたいです。なぜ?


初回のスコア計算に、中心カットスコアを加えるように変更して、再度テストです。


普通に動くだけなのでスクリーンショットはありませんが、今度はうまくいきました。時間差の3つとも同じ数字が出るようになっています。


ただ、また別の不思議な問題が…。

新たな問題 (謎のカットスコア)

バッドカットしたときに、動かない「100」というカットスコアが画面に残り続ける現象が発生しました。いったい何が起こってるんでしょう。


ログを見ると、NoteCutScoreSpawner.csの25行目を起点にして、NullReferenceExceptionが発生しているようです。

----

[CRITICAL @ 08:37:14 | UnityEngine] NullReferenceException: Object reference not set to an instance of an object

[CRITICAL @ 08:37:14 | UnityEngine] (wrapper dynamic-method) FlyingScoreEffect.FlyingScoreEffect.InitAndPresent_Patch2(FlyingScoreEffect,NoteCutInfo&,int,single,UnityEngine.Vector3,UnityEngine.Quaternion,UnityEngine.Color)

[CRITICAL @ 08:37:14 | UnityEngine] (wrapper dynamic-method) FlyingScoreSpawner.FlyingScoreSpawner.SpawnFlyingScore_Patch2(FlyingScoreSpawner,NoteCutInfo&,int,int,UnityEngine.Vector3,UnityEngine.Quaternion,UnityEngine.Quaternion,UnityEngine.Color)

[CRITICAL @ 08:37:14 | UnityEngine] NalulunaFlyingScore.HarmonyPatches.NoteCutScoreSpawnerHandleNoteWasCut+d__1.MoveNext () (at X:/NalulunaFlyingScore/HarmonyPatches/NoteCutScoreSpawner.cs:25)

----


該当のコードを確認すると以下のようになってます。ここでNullReferenceExceptionが発生するということは、flyingScoreSpawnerがnullになっているのでしょうか。

----

flyingScoreSpawner.SpawnFlyingScore(noteCutInfo, lineIndex, multiplierWithFever, noteCutInfo.cutPoint, worldRotation, inverseWorldRotation, new Color(0.8f, 0.8f, 0.8f));

----


ログにflyingScoreSpawnerの状態を表示するようにして再度テストします。


するとログはこの通り。flyingScoreSpawnerは問題ありませんでした。よく見ると、問題の箇所はビートセイバー本体内のFlyingScoreEffect.FlyingScoreEffect.InitAndPresentにあるようです。

----

[DEBUG @ 08:58:23 | NalulunaFlyingScore] {X:\NalulunaFlyingScore\Plugin.cs:23} SpawnFlyingScore:1, flyingScoreSpawner=FlyingScoreSpawner (FlyingScoreSpawner)

[DEBUG @ 08:58:24 | NalulunaFlyingScore] {X:\NalulunaFlyingScore\Plugin.cs:23} SpawnFlyingScore:2, flyingScoreSpawner=FlyingScoreSpawner (FlyingScoreSpawner)

[DEBUG @ 08:58:24 | NalulunaFlyingScore] {X:\NalulunaFlyingScore\Plugin.cs:23} InitAndPresent

[CRITICAL @ 08:58:24 | UnityEngine] NullReferenceException: Object reference not set to an instance of an object

[CRITICAL @ 08:58:24 | UnityEngine] (wrapper dynamic-method) FlyingScoreEffect.FlyingScoreEffect.InitAndPresent_Patch2(FlyingScoreEffect,NoteCutInfo&,int,single,UnityEngine.Vector3,UnityEngine.Quaternion,UnityEngine.Color)

----


dnSpyでこのInitAndPresentを確認します。この中でNullReferenceExceptionを起こしそうなのは…_saberSwingRatingCounter。またこれですか。いちおう特定するため、ログを仕込んでテストします。


テスト後のログを見ると、やはりバッドカットした場合、InitAndPresentの時点でsaberSwingRatingCounterが存在しないようです。

----

SpawnFlyingScore:1, flyingScoreSpawner=FlyingScoreSpawner (FlyingScoreSpawner)

SpawnFlyingScore:2, flyingScoreSpawner=FlyingScoreSpawner (FlyingScoreSpawner)

InitAndPresent: saberSwingRatingCounter=

----


上のほうで、SaberSwingRatingCounterの解放を遅らせる処理を入れたはずですが、バッドカットの場合はそもそも最初から存在しないのか、あるいは別のルートで解放されてしまうのでしょうか。またdnSpyで確認です。


すると…バッドカットの場合は、カットスコアを出す処理自体がキャンセルされていました。上で何も考えずに、この処理の後に2回カットスコアを出す処理を追加していましたが、これがダメでした。バッドカットの場合はこれもキャンセルしないといけません。


これでたぶんOK。テストします。


問題なさそうです。2つ目と3つ目のカットスコアの色が薄いのは気になりますが、どうせ色はmod側で制御するので、気にしないことにします。

youtube post: amXtkya03hw

まだまだ先は長い

これでやっと「カットスコアを時間差で出す」という部分が、完成しました。ここからmodの完成まで持っていくには、まだまだ作らないといけない部分がたくさん残っています。

- 1桁ずつ出すようにする

- 桁ごとに位置をずらして重ならないようにする

- 跳ねるアニメーションを入れる

- スコアごとに色をつける

- 中心カット時の下線を消す

- Proモード

- 設定 (表に見せないもの含む)

- 文字の大きさ、斜体、フォント

- スコアの色、Bloom

- スコアが飛ぶ速度、位置設定

- スコアが飛ぶ時間差設定

- 跳ねるときの初速度、重力、反発係数

- 中心カットスコアのみ表示など

挙げてみると、こんなところでしょうか。

まとめ

一機能を実現する部分だけでもずいぶん長くなってしまいましたが、これでもいろいろと省いて短くした内容なんです。時間差のウエイトを入れた処理を、試しにTaskの別スレッドでやってみたらえらいことになったり、問題の原因究明にぜんぜん見当違いの部分を調べていたり、Twitterが気になって見に行ったっきり戻ってこなくなったり…これは関係ないですね。ごめんなさい。


こんな感じで、mod制作は、動かない → 原因を探るための仕込み → ビートセイバーを起動してテスト → 修正 → やっぱり動かない(最初に戻る)、の繰り返しです。ソロモードの選択まで自動で進むmod(JustTakeMeToTheSoloMode)を作ってしまうのも、わかっていただけると思います。


今回は、かなり込み入った内容で読みにくかったかもしれません。ここまでお付き合いくださって、ありがとうございました!

(YouTube)


(YouTube)


(YouTube)


Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記 Beat Saber mod 制作日記

Comments

今はだいたいの処理を把握しているので、やりたいことを思いついたら、あそこを変えようってわかりますが、1年くらい前は本当に手探りでした。 modの動作確認はそんな感じですね。HMDは動作に必要なとき以外つけないことが多いですけど、アバターmodのキャリブレーション処理の調整なんかでは、何度も付け外しすることになって大変でした。

なるるるるな / NALULUNA

mod制作は地道な作業の連続というのが伺えます。 特に既存(公式)の内容を改変するのは、原因の箇所を探すのが大変そうですね… 開発中にmodの確認をする場合、コード作成 → BeatSaber起動 → HMDでテストプレイ → BeatSaber終了 → ログ確認 → コード修正 → 以下繰り返し…のような流れになるのでしょうか? だとすると、少しの改編だけでも確認が大変だと思います。 試行錯誤して作成したmodを配布していただいて、本当にありがとうございます。

読めるソースコードまで戻るので、比較的楽だとは思います。ネイティブアプリの改変だとアセンブラ読まないといけないのでそれと比べたら…。modは、どうしても試行錯誤が多くなってしまうのと、途中で止めてデバッグのステップ実行的なことができないので、その点はけっこう面倒ですね。

なるるるるな / NALULUNA

mod開発の経験がないのでSDKもAPIも公開されていないものをどう作るのか疑問でした。まさかのデコンパイルですか キーワード検索で改変したい機能の当たりをつけてmodに手を入れて想定通りの動作をするかはトライアンドエラーみたいな流れでしょうか。 コンパイル時に開発者のコメントなんかも全部すっ飛んでると思うので手探りだと気が遠くなりそうですね… NalulunaFlyingScoreは愛用しているので挙動が戻ったらとても嬉しいです

JAN


More Creators