起因和目的
《任务代号美人鱼:莲与深海的姐妹》是一款相当有意思的游戏🤤在美美把玩一段时间后,我发现有些敌人的行为比较影响游戏体验,因此有些打算修改的地方。
BepInEx 框架提供了灵活修改 Unity 游戏内容的方法,可以很方便地实现需要的功能,还可以以扩展的形式即插即用,因此使用了 BepInEx 框架,以及针对游戏修改而构建的 HarmonyX 插件对游戏内容进行修改。
在这次修改中,我想要达成的目标包括:
- 去除飞行的无人机敌人的拘束能力(它们速度太快了,被抓住后手动脱出很浪费时间);
- 缩短肉壁触手破墙而出的反应时间(原本的反应速度太慢,破墙而出之后人早就走了);
- 为肉壁触手添加复活的能力。
创建项目前的准备工作
在创建项目前,还需要进行一点额外的工作:
- 获取游戏的 Unity 版本号;
- 使用合适的 BepInEx 插件初始化环境。
获取 Unity 版本号
获取游戏版本号的方法很简单,在 MissionMermaiden_Data/globalgamemanagers 文件中就有记录。
以文本文件的格式打开 globalgamemanagers 后,在前几行发现 2018.3.03f 的内容,说明 Unity 版本号是 2018.3.0。
初始化 BepInEx
需要使用合适的架构初始化 BepInEx 插件,在 Windows 上,需要确认游戏是 x64 还是 x86 架构。
运行游戏后,在任务管理器中发现 MissionMermainden.exe(32 位),说明游戏基于 x86 架构。
在 BepInEx 的 GitHub Release 中下载对应架构的插件,解压到游戏文件夹中,运行一次游戏并退出,发现 BepInEx 文件夹中出现了额外的配置文件夹。
可以编辑配置文件 BepInEx/config/BepInEx.cfg 中的内容,开启控制台 logging 功能:
1 | [Logging.Console] |
改变设置好后,可以在打开游戏后发现额外的日志输出窗口,便于调试代码。
创建项目
BepInEx 依赖 .NET SDK 进行开发,在 Windows 中,可以在 Visual Studio Installer 中直接安装 .NET 桌面开发 这一工作负荷来获取所需的开发组件。
在安装了工作负荷后,可以在游戏文件夹运行指令获取项目模板并创建项目:
1 | # 获取项目模板 |
其中,MissionMermaidenPlugin 是项目的名称,2018.3.0 是之前获取的 Unity 版本号。
构建项目
创建项目后,可以在项目名称文件夹 MissionMermaidenPlugin 中找到初始化的项目代码,可以使用 Visual Studio 打开 CS 项目文件 MissionMermaidenPlugin.csproj 对项目进行编辑。
添加项目依赖
在项目文件夹中,添加对 HarmonyX 的依赖:
1 | # 添加对 HarmonyX 的依赖 |
导入游戏运行库依赖
在项目依赖项中添加游戏文件夹中的 MissionMermaiden_Data/Managed/Assembly-CSharp.dll 即可导入游戏本体中的内容。
在依赖条目中打开 Assembly-CSharp.dll,即可通过反编译浏览游戏本体中定义的各种类型和函数:
| 导入后的项目依赖结构 | 打开 DLL 后的内容 |
|---|---|
![]() |
![]() |
DLL 中没有注释,不过可以靠名字和代码内容推断,比如上面的 BindJellyManager 很明显是控制水母敌人的类型。
通过对代码进行浏览,发现:
VisorManager是控制飞行无人机的类型;FleshManager是控制肉壁触手的类型。
接下来的代码编写将围绕这两个类型进行。
简单的测试
可以修改初始化后的代码,检查之前开启的控制台日志功能:
1 | namespace MissionMermaidenPlugin |
在编译项目后,将输出目录下的 MissionMermaidenPlugin.dll 和 MissionMermaidenPlugin.pdb 文件移动到 BepInEx/Plugins/ 文件夹下,打开游戏,在日志窗口发现额外的日志输出,说明扩展成功地加载了。
编写代码
HarmonyX 提供了很多方便的方法,将用户定义的函数挂载到游戏的各个位置,可以在类型方法开始前或结束后触发额外的方法,改变类型的内容、改变方法的返回值,甚至可以覆盖原有的方法。
无人机
修改无人机敌人的行为可以提供一个简单的例子,因为使用的成员字段和方法都是公开的:
1 | private class VisorTriggers |
为了方便管理,可以对每个敌人分别设置一个 Triggers 类型进行控制,上面的 VisorTriggers 就是针对无人机的。不要忘记在原本的 Awake 函数中添加应用 Trigger 的语句:
1 | private void Awake() |
在函数前的修饰包括:
[HarmonyPatch(typeof(VisorManager), "OnTriggerEnter2D")]:函数绑定到了VisorManager的OnTriggerEnter2D方法上;[HarmonyPrefix]:函数在绑定的方法调用前进行。
函数的传入参数:
__instance是对运行时VisorManager类变量的引用。- 既然获取了对变量的引用,就可以对其中的变量进行直接修改,比如
__instance.state = VisorManager.STATE.DEAD就直接改变了敌人的状态。 - 通过
__instance获取对象引用的做法是 HarmonyX Patch 参数定义的,也可以按照上述定义传入其它参数,比如获取挂载到的方法的传入参数、劫持返回值等等。
- 既然获取了对变量的引用,就可以对其中的变量进行直接修改,比如
函数的内容:
- 挂载的方法
OnTriggerEnter2D是分析反编译的文件后决定的。这个方法在无人机接触到物体时触发,当state == VisorManager.STATE.CHASE且和主角接触时,无人机将拘束主角; - 调用的
InstantDestroy是VisorManager类中一个独有的方法,可以直接将这个敌人摧毁(大概是因为速度太快很容易出 bug 吧)。
总之,这个额外的方法在无人机企图拘束主角之前直接将其摧毁,在我们和敌人玩耍时不再碍事😎
肉壁触手
对肉壁触手的修改相对来说更加复杂,因为涉及到了很多私有成员字段和方法,也需要对动画相关的逻辑做修改,以实现新的敌人行为(复活)。
缩短反应时间
1 | private class FleshTriggers |
上面的 Trigger 缩短了肉壁触手破墙的反应时间(从原游戏中的 0.5s–2s 缩短到 0.1s–1s)。
Patch 方法返回 true 将仍然调用原本的方法,让游戏按照原本的逻辑处理其他情况下的事件,而返回 false 则将跳过原本的方法(在 HarmonyX 文档中有相关定义)。
对私有成员的操作:
- 读取私有变量:
FleshManager.state等变量是私有的,在读取时需要通过反射的方式获取;FleshManager.state是一个私有的枚举值变量,包括HIDE、IDLE和DEAD三种状态。使用反射访问时,既可以像上面一样转换成定义相同的枚举类型,也可以直接作为int读取。
- 调用私有方法:
FleshManager.Appear是肉壁触手破墙的方法,也是私有的,需要使用Invoke调用。
延迟复活
首先是在敌人被杀死时添加复活的机制:
1 | [] |
这里直接覆盖了原本的 Dead 方法(Patch 方法总返回 false,不再调用原本的 Dead 方法),在复制的原有的事件处理上,添加了随机 1s~2s 后复活的机制(Invoke 破墙的 Appear 方法)。
对私有成员的操作:
- 写入成员字段:大部分字段可以直接使用反射的方法直接写入,但是
__instance.state是一个私有的枚举值类型,需要将写入的状态强制转换成int再写入。
理论上来说,在上面的 Patch 中,一段时间后重新调用 Appear 应当可以实现复活的功能,不需要再对 Appear 进行修改。而在测试中(如下图),敌人被杀死之后虽然的确会复活(出现破墙时的 SE 和动画,靠近也会被拘束),但是外观仍然是被杀死后的样子(变暗并停止动作)。
经过分析后发现上述的问题和 UnityEngine.Animator 有关:Dead 方法中触发了 __instance.animator 的 GoDead 方法,改变敌人的外观;而 animator 没有恢复外观的相关内容。因此,如果不对 animator 做额外操作,敌人的外观将一直是死亡的状态。
因此,需要在复活时额外进行对 __instance.animator 的处理:
1 | [] |
这里在破墙方法 Appear 之前进行了 Patch,如果是复活触发的 Appear 方法,则重置 __instance.animator 的状态,将 animator 状态恢复到初始化时的状态。上述代码中的方法似乎是唯一能够完全重置外观的方法,使用 __instance.ResetTrigger("GoDead") 并不会奏效。
总结
由此,我们缩短了肉壁触手的破墙时间,并为其添加了被杀死后复活的行为。不要忘记在 Awake 中对游戏进行 Patch:
1 | private void Awake() |
编译和测试
和之前的简单测试相同,构建插件并 MissionMermaidenPlugin.dll 和 MissionMermaidenPlugin.pdb 文件移动到 BepInEx/Plugins/ 文件夹下,在游戏中进行测试。
现在在游戏中:
- 飞向主角企图进行拘束的无人机应当在接触到主角时直接消失;
- 隐藏的肉壁触手应当在主角路过后几乎立即破墙而出;
- 肉壁触手在被主角杀死之后,应当在短时间内复活。
好了,不多说了,我要去和触手肉壁玩耍了!

