普段は宇宙人狼こと「Among Us」というゲームでTownOfHostなどのModの開発に携わっています.
AmongUsのMod開発では,HarmonyのフォークであるHarmonyX1というライブラリを利用してパッチを当てることでゲームの動作の変更を行います.
そのパッチの書き方が何通りかあるのですが,それぞれの記法に関して以下に整理してみます.
基本
Harmonyでは,HarmonyPatch
属性を利用してパッチをどのクラスのどのメソッドやプロパティに当てるのかを,HarmonyPrefix
やHarmonyPostfix
属性を利用してパッチの内容を実行するタイミングを指定します.
HarmonyPrefix
は対象のメソッドやプロパティの前,HarmonyPostfix
は後に割り込んで実行されます.
メソッド名をPrefix
やPostfix
とすることで,これらの属性の記述は省略できます.
[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update)), HarmonyPrefix]
// HudManagerというクラスのUpdateというメソッドの前に割り込んで実行される
[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
[HarmonyPrefix]
// 分けて書いても良い
[HarmonyPatch(typeof(HudManager))]
[HarmonyPatch(nameof(HudManager.Update))]
[HarmonyPrefix]
// こういう分け方もできる
[HarmonyPatch(typeof(HudManager), nameof(HudManager.IsIntroDisplayed), MethodType.Getter), HarmonyPrefix]
// プロパティに対するパッチ
なおnameof(HudManager.Update)
は"Update"
と書いても良いですが,本体側のアップデートでパッチ先が消えたりした際にエラーを出すためにnameof
を利用したほうが良いです(コンパイル時に評価されて"Update"
に変換されるのでパフォーマンスに影響はない).
パッチの記法
HudManager
クラスのUpdate
メソッドとToggleMapVisible
メソッドに対して,それぞれの処理の前後に任意の処理を割り当てるパッチについて考えます.
前提
using HarmonyLib;
namespace TownOfExample;
パッチごとに独立したクラスを書く方法
多分アモアスMod界隈で最もメジャーな書き方です.
[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
public static class HudManagerUpdatePatch
{
public static void Prefix() { }
public static void Postfix() { }
}
[HarmonyPatch(typeof(HudManager), nameof(HudManager.ToggleMapVisible))]
public static class HudManagerToggleMapVisiblePatch
{
public static void Prefix() { }
public static void Postfix() { }
}
長所
- みんな使ってる書き方なので万人に読みやすい
- パッチを当てる対象のメソッドが少ない場合に最もシンプルに書ける
- ネストが浅い
短所
- パッチを当てる対象のメソッドが多いと,毎回クラスを作って
typeof(HudManager)
と書くのがまどろっこしい - 複数のパッチからアクセスされるフィールドなどがある場合,置き場所に困る
- 同じクラスに対するパッチがそれぞれ完全に独立して並ぶため,共通化がしづらい
- namespaceの子に直接パッチクラスが並ぶため,他のパッチとの名前被りを避けてクラス名が長くなりやすい
- 基本的に
{クラス名}{メソッド名}Patch
となります - 元々名前が長いメソッドに対して,命名規則を大真面目に守ってパッチを当てようとすると大変なことになります
アモアスで起こりうる極端な例:CustomNetworkTransformIsInMiddleOfAnimationThatMakesPlayerInvisiblePatch
- 基本的に
同じクラスに対するパッチを1つのクラスに入れ子にする方法
アモアスMod界隈ではごく稀に見かける書き方です.
[HarmonyPatch] // このクラスの中にパッチがあることをHarmonyに教えるおまじない
public static class HudManagerPatch
{
[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))]
public static class UpdatePatch
{
public static void Prefix() { }
public static void Postfix() { }
}
[HarmonyPatch(typeof(HudManager), nameof(HudManager.ToggleMapVisible))]
public static class ToggleMapVisiblePatch
{
public static void Prefix() { }
public static void Postfix() { }
}
}
長所
- フィールドや共通メソッドなどの置き場所に困らない
- 同じクラスへのパッチが1つにまとまるのでわかりやすい
- クラス名が長くならない
短所
- パッチを当てる対象のメソッドが少ない場合,いちいちこの書き方をするのはくどい
- ネストが深い
- 別々に書く場合と同様,パッチを当てる対象が多い場合に毎回
typeof(HudManager)
と書くのが面倒
クラスの中にパッチメソッドを直接書く方法
パッチを当てる対象が多い場合では個人的に一番好きな書き方ですが,なぜか自分以外で使ってる人を一度も見たことがありません(なんでだ?).
個別のメソッドごとにパッチを当てるというより,対象のクラスにまるごとパッチを当てる感覚です.
[HarmonyPatch(typeof(HudManager))]
public static class HudManagerPatch
{
[HarmonyPatch(nameof(HudManager.Update)), HarmonyPrefix]
public static void UpdatePrefix() { }
[HarmonyPatch(nameof(HudManager.Update)), HarmonyPostfix]
public static void UpdatePostfix() { }
[HarmonyPatch(nameof(HudManager.ToggleMapVisible)), HarmonyPrefix]
public static void ToggleMapVisiblePrefix() { }
[HarmonyPatch(nameof(HudManager.ToggleMapVisible)), HarmonyPostfix]
public static void ToggleMapVisiblePostfix() { }
}
長所
- クラスを入れ子にする方法の長所を併せ持つ
- ネストが浅い
- この書き方の場合,仕様上
typeof(HudManager)
を書くのは最初の1回だけでOKになる - 対象のクラスに似たクラス構造でパッチクラスを書ける
短所
-
Prefix
Postfix
というメソッド名は使えないので,HarmonyPrefix
HarmonyPostfix
属性を書くのが必須 - 初見だと戸惑うかもしれない
結論
- プロジェクト内で統一して定められていない場合,各シーンで適切なものを選んで使うと良いと思う
- たくさんのパッチを含むクラスで1つ目の別々に書く方法を使うのは,個人的には正直少し読みにくいと感じる
- 逆に,単一のメソッドにちょっと処理を付け足したいような場合は1つ目の方法が適していると思う
- 2つ目のクラスの中にクラスを入れる書き方は正直利点がよくわからないなあと思いながら使ってる
イメージとしては,「メソッドに対するパッチ」を書くなら1つ目,「クラスに対するパッチ」を書くなら3つ目かな,というのが今のところの結論です.
-
簡単のため両者を区別せず「Harmony」と呼ぶことにします ↩