普段はTown of MossというMODの開発をしていますが、気が向いたらAmong UsのMODの作り方について散発的にメモしていこうと思います。
これはv12.15の対応に難航しすぎて暇になったためです。
MODとは
MODとはModifierの略で、既存のゲームプログラムに手を加える技術のことです。
Among Usは非常に優れたシステムのゲームでありつつ絶妙にかゆいところがあるので、勝手に書き換えると楽しいです。
インポスターをキルできるSheriff役職のような、クルーメイトとインポスターだけでない固有の能力を持った役職を追加することが主に行われていますが、コミュニケーションサボタージュ時にクルーメイトの外見で区別がつかなくなったり、会議を挟んでもインポスターのキルクールダウンをリセットさせなくしたりといったルール自体への介入も可能です。
一時期Polus.ggという非公式MODがSteamに登場したりしていました。
必要なもの
- Unity知識
- C#知識
- WIndows環境
- git知識
- C#用IDE(Jetbrains Rider(有料)またはVS Code(無料))
- Steam版Among Us
- Among UsのAssembly-CSharp.dll 正規の入手方法はわかりませんがReactorのDiscordサーバで入手することができます。
- Unity Explorer pluginsフォルダに配置すると、実行中にF7を押すことでプログラム中のオブジェクトの情報を確認、状態の変更などができます。
- DnSpy Among UsのAssembly-CSharp.dllファイルからAmong Usのソースコードを復元できます。
以下の動画を参考にBepInExやReactorをインストールします。
https://www.youtube.com/watch?v=47U1PJ0RVvo
動画では触れられていませんが開発の際はReactor.Debugger.dllも入れておくと、F1で便利機能がちょっとだけ使えます。
Reactorをインストールするとサンプルのテンプレートが入っています。
また、大手のMODプロジェクトがオープンソースで開発されているため、Forkすればすぐに既存MODの改変を始めることができます。
Town of Moss
Town of Usをベースに作り始めた僕のプロジェクトです。
11.9.5sの大型アップデートの対応をなんとかしのぎきったところで、12.15sへの対応方法がわからず取り残されています。
Town of Us R
6.30で更新が停止したTown of Usのあとを継いだプロジェクトです。
最近脱Reactorしました。12.15sではカスタムサーバでのみ動作するらしい?
The Other Roles
YouTubeなどでは一番よく見るタイプのMODだと思います。
部分的に参考にしていますが、Reactor以前から存在していたこともありビルドの仕方は正直よくわかっていません。
バージョンについて
アップデートがあった場合などは、NuGetを用いて対応するバージョンのライブラリに切り替えます。
HarmonyPatchについて
MOD開発では、Harmonyを用いて、基本的に従来のプログラムのメソッドの呼び出し前後に任意のコードを追加することで動作に介入します。
特に頻出するクラスやメソッドは以下のようなものがあります。
- PlayerControl.FixedUpdate:各プレイヤーに対して毎フレーム行う処理
- HudManager.Update:ボタンなどのUIに関連する処理
- KillButton.DoClick:キルボタンを押した際の処理
基本
例として、全てのクルーメイトがサボタージュを行えるパッチを作ります。
Among Us 12.15sにおいて、マップ表示はMapBehaviourクラス内の以下の処理で行われます。
プレイヤーがインポスターであればサボタージュ用のマップを表示する仕組みになっていることがわかります。
public void ShowNormalMap()
{
//略
if (PlayerControl.LocalPlayer.Data.Role.IsImpostor)
{
this.ShowSabotageMap();
return;
}
//略
}
この処理の間だけインポスター陣営扱いにするパッチを作ります。
Prefixメソッドは指定されたメソッドの前に割り込んで呼び出され、Postfixメソッドは指定されたメソッドのあとで呼び出されます。
引数は、メソッドを持つクラスの型で__instanceという名前にし、これを本来のthisの代わりに用います。
また、Prefixの返り値をboolとして宣言すると、falseを返すことで本来のメソッドとPostfixを呼び出さずに処理を終わらせることができます。
従来の処理を丸ごと置き換えたり、条件を追加する場合などに用います。
Prefixで、インポスターでない場合は変更フラグを立ててインポスター陣営にし、Postfixではフラグを確認し元に戻します。
ref bool __stateとして宣言した値は、Prefixで代入してPostfixで参照できるようになります。
これはShowSabotageMapPatchのメンバとして宣言しても同様の使い方ができるような気がするので必然性はわかってません。
[HarmonyPatch(typeof(MapBehaviour), nameof(MapBehaviour.ShowNormalMap))]
public static class ShowSabotageMapPatch {
public static void Prefix(MapBehaviour __instance, ref bool __state) {
__state = false;
if (MeetingHud.Instance) {
if (!PlayerControl.LocalPlayer.Data.IsImpostor()) {
PlayerControl.LocalPlayer.Data.Role.TeamType = RoleTeamTypes.Impostor;
__state = true;
}
}
}
public static void Postfix(MapBehaviour __instance, ref bool __state) {
if (MeetingHud.Instance) {
if (__state) {
PlayerControl.LocalPlayer.Data.Role.TeamType = RoleTeamTypes.Crewmate;
}
}
}
}
引数を用いる場合
Among Usでは、キルや通報、タスクの完了など、他のクライアントに座標以外の情報を伝えたいとき、RPCというプロトコルを用います。
//Among UsでのHandleRPCの定義
public abstract void HandleRpc(byte callId, MessageReader reader);
MODで追加される様々な役職の能力を実行する際などにも、元々存在しているHandleRpcメソッドにPostfixメソッドを追加し、RPCのIDに応じた処理を行います。
このとき、HandleRpcメソッドに渡されていた引数を使用する必要があります。
PrefixやPostfixの引数にHarmonyArgumentプロパティを追加することで、パッチ先のメソッドの引数を利用することができます。
HarmonyArgument(0)では1番目の、HarmonyArgument(1)では2番目の引数を使用します。
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))]
public static class HandleRpc
{
public static void Postfix([HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader)
{
//略
ログ
ログは初期設定ではBepInExディレクトリ内に生成されるので、好きなコマンドラインツールでtailします。
λ tail -f LogOutput.log LogOutput.1.log LogOutput.2.log LogOutput.3.log LogOutput.4.log LogOutput.5.log
つまづきがちなポイント
プログラム中で使われるListやDictionaryなどのコレクションクラスにおいて、System.Collections.GenericのものとIl2CppSystem.Collections.Genericのものが存在するため、不用意に宣言すると混乱を招きます。
また、DateTimeクラスもSystem.DateTimeとIl2CppSystem.DateTimeが混在することがあり、宣言の場合は注意が必要です。