Unity游戏Mod/插件制作教程07 - Harmony补丁实战:拦截、修改与状态传递
1. Harmony补丁实战入门:从拦截到修改
如果你已经跟着前几期教程学会了基础插件开发,现在应该能用C#代码实现一些简单功能了。但你可能发现了问题——很多功能需要主动触发(比如按键操作),而我们更希望Mod能自动运行。这时候就该Harmony登场了,它能让我们在不修改游戏源码的情况下,直接干预游戏方法的执行流程。
上周我们学会了用[HarmonyPatch]标记补丁目标,这次要深入两个实战场景:方法拦截修改和状态传递。想象你要做个"无限耐力"Mod,当角色奔跑时,我们需要:
- 拦截游戏计算耐力消耗的方法
- 修改传入的耐力值参数
- 在不同补丁方法间传递自定义状态
// 示例:修改角色耐力值 [HarmonyPatch(typeof(PlayerController), "UpdateStamina")] class InfiniteStaminaPatch { public static void Prefix(ref float staminaCost) { staminaCost = 0; // 所有耐力消耗归零 } }2. Prefix补丁的四种拦截姿势
2.1 基础参数修改
最常见的场景是修改方法参数。比如游戏里有这样计算伤害的方法:
public float CalculateDamage(float baseDamage, float armorFactor) { return baseDamage * (1 - armorFactor); }我们可以用Prefix补丁强行修改伤害值:
[HarmonyPatch(typeof(DamageSystem), "CalculateDamage")] class GodModePatch { public static void Prefix(ref float baseDamage) { if(Input.GetKey(KeyCode.F1)) baseDamage = 9999; // 按下F1开启秒杀模式 } }2.2 完全拦截方法执行
通过返回false可以阻止原方法运行,这在实现"无敌模式"时特别有用:
[HarmonyPatch(typeof(PlayerHealth), "TakeDamage")] class NoDamagePatch { public static bool Prefix() { return false; // 拦截所有伤害 } }2.3 条件拦截
更聪明的做法是根据游戏状态决定是否拦截。比如只当生命值低于20%时触发自动回血:
[HarmonyPatch(typeof(PlayerHealth), "Update")] class AutoHealPatch { public static bool Prefix(PlayerHealth __instance) { if(__instance.health < __instance.maxHealth * 0.2f) { __instance.health += Time.deltaTime * 5; return false; // 拦截原更新逻辑 } return true; } }2.4 修改返回值
虽然Prefix拿不到方法返回值,但我们可以直接给结果变量赋值:
[HarmonyPatch(typeof(Inventory), "GetItemCount")] class InfiniteItemsPatch { public static bool Prefix(ref int __result) { __result = 999; // 获取物品数量永远返回999 return false; } }3. 跨补丁的状态传递技巧
3.1 __state参数的妙用
有时候我们需要在Prefix和Postfix之间传递数据。比如记录方法执行前的状态:
[HarmonyPatch(typeof(EnemyAI), "Update")] class EnemyAIPatch { public static void Prefix(out Vector3 __state) { __state = EnemyAI.lastKnownPosition; // 保存原始值 } public static void Postfix(Vector3 __state) { if(Vector3.Distance(__state, EnemyAI.lastKnownPosition) > 10) Debug.Log("敌人位置发生剧烈变化"); } }3.2 复杂状态对象
__state不仅限于基础类型,可以传递任意对象:
class AttackState { public float originalDamage; public bool wasCritical; } [HarmonyPatch(typeof(Weapon), "CalculateCriticalHit")] class CriticalHitPatch { public static void Prefix(ref AttackState __state, float baseDamage) { __state = new AttackState { originalDamage = baseDamage, wasCritical = UnityEngine.Random.value > 0.9f }; } public static void Postfix(AttackState __state, ref float __result) { if(__state.wasCritical) __result = __state.originalDamage * 2.5f; } }3.3 状态生命周期管理
注意__state的生命周期只在当前方法调用期间有效。如果需要持久化状态,应该使用静态变量:
[HarmonyPatch(typeof(GameManager), "SaveGame")] class SaveGamePatch { private static DateTime lastSaveTime; public static void Prefix() { lastSaveTime = DateTime.Now; } public static void Postfix() { Debug.Log($"存档耗时:{(DateTime.Now - lastSaveTime).TotalMilliseconds}ms"); } }4. 实战:制作无限耐力Mod
现在我们来完整实现一个经典案例。假设游戏中有个PlayerMovement类处理移动逻辑:
public class PlayerMovement : MonoBehaviour { public float stamina = 100; public float maxStamina = 100; void Update() { if(Input.GetKey(KeyCode.LeftShift) && stamina > 0) { stamina -= Time.deltaTime * 10; MoveFaster(); } else if(stamina < maxStamina) { stamina += Time.deltaTime * 5; } } }4.1 方案一:直接修改耐力值
最简单的实现方式是在耐力减少时反向增加:
[HarmonyPatch(typeof(PlayerMovement), "Update")] class InfiniteStaminaPatch1 { public static void Postfix(PlayerMovement __instance) { if(__instance.stamina < __instance.maxStamina) __instance.stamina = __instance.maxStamina; } }4.2 方案二:拦截消耗逻辑
更优雅的做法是拦截耐力消耗逻辑:
[HarmonyPatch(typeof(PlayerMovement), "Update")] class InfiniteStaminaPatch2 { public static bool Prefix(PlayerMovement __instance, ref float __state) { __state = __instance.stamina; // 保存当前值 // 如果按下冲刺键但耐力不足 if(Input.GetKey(KeyCode.LeftShift) && __instance.stamina <= 0) { __instance.stamina = 1; // 保持最小正值 return true; // 继续执行原方法 } return true; } public static void Postfix(PlayerMovement __instance, float __state) { // 如果耐力被消耗 if(__instance.stamina < __state) { __instance.stamina = __state; // 恢复原值 } } }4.3 方案三:完全自定义逻辑
最高级的实现是接管整个更新逻辑:
[HarmonyPatch(typeof(PlayerMovement), "Update")] class InfiniteStaminaPatch3 { public static bool Prefix(PlayerMovement __instance) { // 完全重写Update逻辑 if(Input.GetKey(KeyCode.LeftShift)) { __instance.MoveFaster(); } return false; // 阻止原Update执行 } }5. 调试与常见问题排查
5.1 补丁未生效检查清单
- 确认Harmony初始化:确保插件启动时调用了
Harmony.CreateAndPatchAll - 检查方法签名:参数类型和数量必须完全匹配
- 验证访问权限:非public方法需要正确设置BindingFlags
- 查看Harmony日志:在初始化代码后添加
FileLog.Reset()开启日志
5.2 典型错误案例
案例一:参数类型不匹配
// 错误示范:漏了ref关键字 [HarmonyPatch(typeof(Player), "SetHealth")] class Patch1 { public static void Prefix(int health) {} // 应该用ref int } // 正确写法 [HarmonyPatch(typeof(Player), "SetHealth")] class Patch2 { public static void Prefix(ref int health) {} }案例二:混淆实例方法与静态方法
// 错误示范:静态方法误用__instance [HarmonyPatch(typeof(Utilities), "StaticMethod")] class Patch3 { public static void Prefix(object __instance) {} // 静态方法没有实例 } // 正确写法 [HarmonyPatch(typeof(Utilities), "StaticMethod")] class Patch4 { public static void Prefix() {} }5.3 性能优化建议
- 减少补丁数量:合并多个补丁到一个Patch类
- 避免频繁分配内存:特别是在Update方法中
- 使用缓存:对反射获取的MethodInfo进行缓存
- 条件补丁:通过
[HarmonyBefore]、[HarmonyAfter]控制执行顺序
// 优化示例:缓存MethodInfo private static MethodInfo targetMethod; [HarmonyPatch] class OptimizedPatch { static MethodBase TargetMethod() { return targetMethod ??= AccessTools.Method( typeof(SomeClass), "SomeMethod", new[] { typeof(int), typeof(string) } ); } }6. 高级技巧:动态补丁
有时候我们需要根据游戏状态动态启用/禁用补丁:
Harmony harmony; Patches staminaPatch; void Start() { harmony = new Harmony("com.my.mod"); staminaPatch = new Patches(); TogglePatch(false); } void TogglePatch(bool enable) { if(enable) harmony.Patch( typeof(PlayerMovement).GetMethod("Update"), prefix: new HarmonyMethod(staminaPatch.GetType().GetMethod("Prefix")) ); else harmony.Unpatch( typeof(PlayerMovement).GetMethod("Update"), HarmonyPatchType.Prefix, "com.my.mod" ); } class Patches { public void Prefix() { ... } }7. 最佳实践与安全建议
- 命名空间隔离:为补丁类使用独立命名空间
- 异常处理:补丁方法内要有try-catch块
- 版本兼容:使用
[HarmonyBefore]处理Mod冲突 - 性能监控:记录补丁执行时间
- 配置化:通过配置文件控制补丁行为
[HarmonyPatch(typeof(PlayerMovement), "Update")] class ConfigurablePatch { public static bool Prefix() { if(!ModSettings.EnableInfiniteStamina) return true; // 补丁逻辑... return false; } }记得在实际项目中,过度使用Harmony补丁可能会导致游戏不稳定。我个人的经验法则是:能用常规Unity事件系统实现的就不用Harmony,必须修改游戏原有逻辑时才使用补丁技术。
