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必须修改游戏原有逻辑时才使用补丁技术。