从零构建Rimworld Mod:项目源码与Harmony补丁实战

📅 发布时间:2026/10/14 4:51:11
从零构建Rimworld Mod:项目源码与Harmony补丁实战
简介面向环世界Rimworld模组开发入门者以及希望了解游戏 Mod 工程结构的编程爱好者这份项目源码包以“瘟疫测试枪”为完整案例从 About 目录、Defs 数据定义到 Assemblies 程序集搭建出可运行的 Mod 骨架结构上高度贴合官方推荐规范。读者配合 Visual Studio 或 JetBrains Rider 打开工程后可逐行研读 C# 代码与 XML 配置依次学习程序集构建、About/Defs 简介修改、本地化翻译并通过反编译游戏代码理解内部逻辑掌握从编写到测试的完整排错思路最终得到一把可实际装入游戏的瘟疫测试枪。包内共 11 个文件以 C# 源文件与 XML 数据定义为主另有 csproj 工程文件、gitignore 忽略规则、Markdown 说明和 HTML 预览页等压缩包整体仅 16KB轻量、无冗余便于逐文件对照教程学习。目前已有 212 人学习下载尤其适合刚接触模组开发、想获得可直接运行和改造示例工程的读者快速上手并进入实战。1. Rimworld Mod制作为什么项目源码比单个Defs文件更有价值Rimworld Mod制作很多人第一反应是改XML数值或者复制现成的补丁但真正能把Mod做成自己的东西、能长期维护的人几乎都是从一份完整项目源码起步的。项目源码意味着C#工程、Defs目录、About清单、Harmony补丁层被统一组织起来临时改一个Def只能算配置源码级开发才能控制游戏逻辑本身。这份教程从一份可复现的DemoMod出发拆解源码结构、C#与XML的分工、Harmony挂载方式以及最容易翻车的五类问题。它适合想从复制粘贴Defs升级到自己写逻辑的Mod作者也适合想理解Rimworld Mod运行机制、准备做深度定制的从业者。整个Mod的复杂度由项目源码决定这也是这篇文章标题里项目源码四个字的分量所在。2. 搭建Rimworld Mod开发环境从游戏目录到IDE的完整配置2.1 先搞清Mod运行的三个容器Rimworld Mod运行环境没有常规意义的插件入口。游戏启动时引擎会扫描Mods目录下每个Mod文件夹按mod列表顺序加载Assemblies里的DLL同时解析Defs目录下的XML。这带来三个直接后果DLL不需要Main函数游戏会调用所有标注了[StaticConstructorOnStartup]的静态构造函数XML里定义的ThingDef、RecipeDef等在C#代码里会被还原成对应类型的对象日志统一写到玩家专属目录下的output_log.txt。理解这三点你就明白为什么很多老Mod作者强调环境搭建决定成败——不是IDE配置有多难而是加载机制和你的直觉不一样。配置分四步走。第一找到游戏安装目录把游戏根目录作为基准。第二确认Mod文档结构Mod必须包含About/About.xml来完成识别否则游戏连名字都认不出来。第三创建C#类库工程目标框架选net48如果IDE没有.NET Framework选项可以直接改.csproj里的TargetFrameworkVersion。第四引用游戏程序集把游戏目录下Managed文件夹里的Assembly-CSharp.dll、UnityEngine核心DLL、0Harmony.dll加进引用。注意Unity版本不同Managed目录下的文件清单会有差异新手最容易犯错的是只引用Assembly-CSharp.dll却忘掉它的依赖项导致编译通过但运行时报TypeLoadException。配置完成后Mod目录结构大致是这样Mods/DemoMod/ ├── About/ │ ├── About.xml │ └── Preview.png ├── Assemblies/ │ └── DemoMod.dll └── Defs/ └── Things/ └── DemoDefs.xml上面是第一层布局About是识别文件游戏靠它显示Mod名、作者与版本Assemblies放编译产物Defs放XML定义。第一版Demo不必做Patches目录但Defs和Assemblies两个目录一定不能缺否则游戏会认为这是个坏Mod。很多人在这一步翻车是因为在IDE里建的是控制台程序结果编译出一个带Main入口的exe放进Assemblies后游戏加载时没有任何反应日志也不报错最后才发现方向从一开始就错了。2.2 创建最小C#工程从csproj到第一个DLL我建议直接用类库模板不要选控制台程序。控制台模板默认生成Program.Main而Rimworld不需要这个入口反而容易引入无关引用。把.csproj里的OutputPath指向Mod的Assemblies目录是省去每次手动拷贝的习惯动作。下面是最小csproj的关键片段Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknet48/TargetFramework OutputPath..\Mods\DemoMod\Assemblies/OutputPath /PropertyGroup ItemGroup Reference IncludeAssembly-CSharp HintPath..\RimWorld\RimWorld_Data\Managed\Assembly-CSharp.dll/HintPath Privatefalse/Private /Reference Reference Include0Harmony HintPath..\RimWorld\RimWorld_Data\Managed\0Harmony.dll/HintPath Privatefalse/Private /Reference /ItemGroup /ProjectTargetFramework指定为net48对应Rimworld使用的.NET Framework版本如果编译报不支持目标框架确认IDE里装了对应开发包。OutputPath直接指向DemoMod的AssembliesPrivate设为false表示不在输出目录复制游戏DLL防止Mod发出去时把一堆几十MB的程序集打包进去。HintPath是相对路径建议用相对路径而不是绝对路径否则你换目录或换电脑就要改一次引用。下一步写一个能验证管道通关的类。通常的做法是放一个启动类注册一个Harmony自动补丁using System; using HarmonyLib; using RimWorld; using Verse; namespace DemoMod { [StaticConstructorOnStartup] public static class Startup { static Startup() { var harmony new Harmony(com.example.demomod); harmony.PatchAll(); } } }这个类没有入口方法[StaticConstructorOnStartup]告诉游戏在启动阶段执行静态构造函数。Harmony实例的ID参数是全局唯一标识建议用作者名.项目名格式同一个ID重复实例化会报错。PatchAll会自动扫描当前程序集里所有[HarmonyPatch]标注的类后续加补丁只需要写新类不用再手动注册。这段代码是项目源码和零散文件目录的最大区别一切挂载都集中在一处而不是靠每个文件自己初始化。编译后在Assemblies目录会生成DemoMod.dll。把Mod目录放到游戏Mods下在游戏主菜单Mod列表里勾选进入游戏如果没报红字、Mod列表能显示名称和版本环境就算通关。这里有个经常被忽略的动作改完DLL重新编译后必须完全退出游戏再重进游戏不会在运行中热加载新的程序集。2.3 用About.xml完成Mod身份证About.xml是Mod的身份证游戏要求它必须存在关键字段不能错。一个最小可识别版本?xml version1.0 encodingutf-8? ModMetaData nameDemoMod/name author你的名字/author packageIdcom.example.demomod/packageId supportedVersions li1.5/li /supportedVersions description用于学习Rimworld Mod制作的DemoMod/description /ModMetaDataname显示在Mod列表里packageId必须全局唯一两个Mod用同一个ID会互相冲突直接导致其中一个不加载。supportedVersions不是随便填的填一个不存在的版本游戏在加载时发现版本不匹配会把这个Mod标成不兼容并跳过这里需要替换为你游戏对应的版本号。description可以写长一点它显示在Mod详情框里。此外Mod列表的加载顺序由loadAfter和loadBefore控制这个字段在正式Mod里几乎必填因为很多Mod需要在本体加载后再执行自己的补丁。到这里环境搭建结束。真正的开发环境不只是IDE而是游戏目录 Mod目录 工程文件三者的协调项目源码的价值在这一步已经开始体现。接下来进入源码核心Harmony。3. 拆解Harmony与C#源码从补丁机制到第一个可运行逻辑3.1 Harmony为什么是Rimworld Mod的核心任何Rimworld Mod教程绕不开Harmony它本质是一个运行时方法补丁库。基本原理是在某个原版方法执行前、执行后、甚至替换整个方法时插入你的代码。Rimworld没有官方Mod扩展接口社区的做法就是通过Harmony给游戏方法打补丁。你会遇到Prefix、Postfix、Transpiler这几个概念。Prefix在原方法之前执行适合做条件拦截Postfix在原方法之后执行适合修改返回值Transpiler直接改IL中间代码不推荐新手用因为每次游戏更新都可能让IL失效。看一段能直接用的补丁代码让所有殖民者的移动速度提升10%。using HarmonyLib; using Verse; namespace DemoMod { [HarmonyPatch(typeof(Pawn))] [HarmonyPatch(nameof(Pawn.MoveSpeed), MethodType.Getter)] public static class Patch_Pawn_MoveSpeed { static void Postfix(Pawn __instance, ref float __result) { if (__instance.IsColonist) { __result * 1.1f; } } } }[HarmonyPatch]标注目标类、成员名和成员类型。MoveSpeed是属性所以MethodType.Getter表示补丁的是属性的get访问器。Postfix接收两个特殊参数__instance表示被补丁的实例对象ref float __result表示原方法的返回值你可以读取并修改它。这段演示了核心思想不改游戏文件、不破坏存档在运行时动态改变逻辑。Harmony的补丁是叠加的多个Mod可以同时补同一个方法执行顺序由加载顺序决定这也是Mod之间冲突的主要来源。有一点需要特别说明补丁类必须是静态类方法必须为static。如果写成实例方法Harmony会在应用补丁时直接跳过且不给出明显报错。另一个新手容易踩的点是方法签名过于具体原方法有重载时你还需要提供参数类型数组来精确指定目标重载否则Harmony匹配失败。3.2 从源码角度理解Mod的初始化生命周期一个Mod从启动到运行源码层面的生命周期大概是游戏加载程序集执行[StaticConstructorOnStartup]静态构造函数Harmony.PatchAll扫描并挂载所有补丁随后Defs被解析成对象游戏实例化并运行。这里面容易忽略的一个点是在[StaticConstructorOnStartup]里访问Defs多半是不安全的因为XML Defs的加载顺序在PatchAll之后。想在启动时读取某个ThingDef正确做法是延迟到第一次需要访问的时机而不是在构造函数里直接查。我见过不少Mod作者在这里把逻辑写死结果游戏启动随机红字重启几次又正常被当成玄学问题。实际上原因是Defs尚未加载完成。稳妥的做法是定义一个GameComponent把Mod级状态放在它的ExposeData和Tick里而不是塞进静态字段。静态字段虽然写着方便但存档加载、热重载时它的生命周期完全不受控。来看一个延迟初始化的推荐写法using Verse; namespace DemoMod { public class DemoGameComponent : GameComponent { private bool initialized; private ThingDef cachedDef; public DemoGameComponent(Game game) : base(game) { } public override void GameComponentTick() { if (!initialized) { cachedDef DefDatabaseThingDef.GetNamed(DemoMod_Machete); initialized true; } } } }GameComponent是Rimworld提供的一个随存档保存的游戏级组件它的Tick方法在游戏运行期间每帧被调用这也意味着你可以在组件里做状态管理。在这个例子里第一次Tick时才去查DefDatabase确保Defs已经加载完成。注册GameComponent的方式是在Mod根目录加一个GameComponent的Def或者用Harmony补丁一个初始化点我通常用后者避免多写一个XML文件。3.3 编译、加载与日志验证判断补丁是否生效补丁写完后最直接的验证方式是看游戏日志。日志会输出Harmony的相关信息在日志里搜你的harmony ID或者补丁类名能看到补丁是否被标记为applied。但这类信息通常不够直观我更推荐的做法是在Postfix里写一行Log.Message进入游戏触发该逻辑然后打开日志文件搜索标记内容。如果日志里有输出说明补丁挂载成功且逻辑执行了如果没有检查类名拼写、方法签名是否匹配、补丁是否被其他Mod的加载顺序影响。日志是Mod调试唯一不撒谎的黑匣子。Rimworld把游戏内Log窗口的输出同时写入文件路径在玩家专属目录下的Logs文件夹。找不到时打开游戏内的Debug Actions里面有OpenLogFolder直接定位到日志所在位置。这一步对后续所有排查都至关重要第5章的每一条踩坑记录几乎都要回到日志上做判断。4. 用XML Defs定义内容从ThingDef到配方源码联调4.1 Defs不是配置文件是对象初始化器很多新手把Rimworld的XML Defs当成配置文件这个误解会带来很多坑。实际上每个Def节点在游戏启动时会被反序列化为一个对应的C#对象比如ThingDef节点最终变成ThingDef类的实例节点里的defName、label、description等子节点会填充到对象的对应字段。这个机制决定了你写XML时节点名必须与C#类的公开字段名匹配拼错一个字母游戏不会报编译错误而是在日志里抛Field not found或直接忽略该值。来看一个自定义近战武器的完整Def?xml version1.0 encodingutf-8? Defs ThingDef ParentNameBaseWeapon_Melee defNameDemoMod_Machete/defName labeldemo砍刀/label description一把用于教学的砍刀。/description thingClassThingWithComps/thingClass statBases MeleeWeapon_DamageAmount12/MeleeWeapon_DamageAmount MeleeWeapon_CooldownMultiplier1.2/MeleeWeapon_CooldownMultiplier /statBases tools li labelslash/label capacities liCut/li /capacities power20/power cooldownTime1.5/cooldownTime /li /tools /ThingDef /DefsParentName表示继承哪个原版模板BaseWeapon_Melee是原版近战武器的基类继承它会自动获取武器的大部分基础字段你自己定义的子节点会覆盖或补充继承来的值。thingClass决定这个对象在游戏里的行为类。statBases里的参数对应游戏里的基础属性参数名必须与StatDef的defName一致写错只会在日志里出现一条Unknown StatDef。tools节点定义武器的攻击方式capacities里的Cut决定它能切割植物还是割伤肢体这个字符串也必须与游戏里的DamageDef对应。注意XML文件编码。如果有中文注释或label保存为UTF-8带BOM或UTF-8无BOM都可能遇到读取问题我的习惯是统一用UTF-8编码并在文件头部不加任何多余字符。有些编辑器默认存成GBK游戏加载后label、description变成乱码这类问题在日志里看不出来只能靠你看到游戏内文字异常来定位。4.2 为自定义物品写RecipeDef和对应的C#逻辑光有武器Def还不够要让殖民者能制造它还需要RecipeDef定义制作配方。RecipeDef的关键字段包括工作台类型、材料清单、产物和权限要求。下面是一份能配合原版锻造台使用的配方RecipeDef defNameDemoMod_MakeMachete/defName jobString制作demo砍刀。/jobString workAmount150/workAmount workSpeedStatRecipe_Speed/workSpeedStat ingredients li filter thingDefs liSteel/li /thingDefs /filter count20/count /li /ingredients products liDemoMod_Machete/li /products recipeUsers liTableMachining/li /recipeUsers /RecipeDefworkAmount是工作总量数值越大制造越慢它除以制作速度得到实际时间。ingredients定义材料需求filter限定允许的材料类型这里限定为钢。count是单次制造的消耗数量。products一定要写DefName而不是label它必须与之前ThingDef里的defName完全一致这个字符串不一致是最常见的翻车原因现象是配方制造完毕时没有产出。recipeUsers是允许执行该配方的实体DefNameTableMachining对应原版机械加工台不写这一项殖民者可能没有任何工作台能执行这个配方。如果你的武器需要额外逻辑比如挥砍时附带火焰伤害就需要在ThingDef里加comps节点并在C#端写一个CompTargetEffect或CompUseEffect。C#侧与XML的联调关键在字符串对应关系XML里的compClass属性值必须是你的C#类的完整命名空间。这里最容易出的问题是改了C#类名后忘记同步改XML或者把命名空间写错游戏加载Def时找不到类型整个Def被丢弃。4.3 用Abstract和Patch机制降低多个Mod共存时的爆炸概率当你需要定义十种不同伤害的武器时重复复制整段XML是灾难。Rimworld提供了Abstract和继承机制先定义一个Abstract基类再让具体Def继承它。实践习惯是每个Mod建一个AbstractDefs.xml把所有共用参数下沉到基类具体Def只写差异字段。这样项目源码更清晰也为后续内容扩展留了位置。另一个重要机制是Patch文件它允许在不新建Def的情况下修改原版Def比如把原版武器的伤害从5改成8。基本结构是Operation.Add、Operation.Replace和Operation.Remove。与直接定义Def相比Patch方式有更好的兼容性直接定义同名Def后加载的Mod会覆盖先加载的用Patch方式做细粒度修改两个Mod各改各的字段冲突概率显著降低。多Mod场景下正式Mod几乎都会用Patch而不是直接覆盖原版内容。5. Rimworld Mod避坑指南日志与行为两级排查5.1 日志型翻车补丁没生效、类型加载失败、字段认不出第一条记录现象是补丁写完了但游戏行为没有任何变化日志里也没有红色报错。原因通常有两个一是[HarmonyPatch]标注的方法签名不匹配目标方法名拼错或者参数列表不一致Harmony静默跳过二是harmony.PatchAll没有被调用或者harmony ID冲突导致抛出的异常被你忽略了。解决方法是先检查Startup类是否存在且被[StaticConstructorOnStartup]标注然后在Postfix第一行写Log.Message(patch reached)重新编译并进游戏触发该逻辑再查日志有没有这条输出。如果连日志都没有问题就在挂载环节去查方法签名。第二条记录现象是编译通过运行时报TypeLoadException或者找不到类型。这通常是引用程序集不完整导致的比如你的代码用了UnityEngine里的数学类型但csproj只引用了Assembly-CSharp.dll没引用UnityEngine核心DLL。解决方法是回到csproj把Managed目录下的必要DLL都加进去同时确认TargetFramework是net48。还有一个隐蔽情况IDE的引用列表里显示DLL存在但HintPath指向了旧版本的Managed目录导致加载了错误的程序集。检查HintPath是否仍然指向当前游戏版本的目录。第三条记录现象是Defs里的字段不起作用日志里出现Unknown StatDef或Unknown Def。原因是XML节点名与C#类的字段名不匹配或者定义在了错误的父节点下。解决方法是打开日志搜索完整的报错字段名再对照反编译工具看Def类的公开属性。这里需要强调一个习惯不要凭猜补字段日志会明确告诉你是哪个Def的哪个字段没被识别照着改很少需要第二次猜测。5.2 行为型翻车Mod被跳过、更新后变红字第四条记录现象是Mod列表里能看到名称但游戏启动时Mod被标成不兼容或者直接跳过。常见原因是About.xml里的supportedVersions写得与实际游戏版本不符或packageId与另一个Mod重复。有些Mod作者为了省事直接复制别人的About.xml连packageId都原样保留这是最典型的操作。解决方法是packageId统一用你自己的反域名格式supportedVersions按照当前版本填写如果不确定格式先只写一个主版本号。第五条记录现象是游戏更新后之前正常的Mod开始报红字同一套源码没有改动。原因是游戏程序集API变化原方法签名变了Harmony补丁还按旧签名挂载匹配失败。解决方法是重新检查被补丁方法的签名这种情况最常出现在方法参数类型变化或返回值类型变化。我的建议是凡是补丁了原版方法就在源码里注释里记录补丁目标方法名 所在游戏版本 签名三条信息。游戏更新时优先检查这些方法是否变动。另一个能够降低这类问题频率的做法是避免补丁到过于底层的热路径方法优先补丁封装的业务方法API相对稳定。6. 让源码自己说话验证Mod与跨版本维护的进阶习惯Mod做完能跑只算完成一半另一半是下一次游戏更新后还能不能跑。这个习惯是从某次翻车里学来的一个DemoMod补丁了某个原版方法当时一切正常过了一个月游戏更新后红字一片反编译一看原方法加了新参数旧补丁直接失配。从那以后我要求自己的每个补丁都带上目标方法的签名记录放到源码注释的第一行。验证方法按三层来做。第一层是每次编译后用最小测试存档进游戏只勾选自己的Mod触发一次被修改的逻辑看日志有没有新增输出。第二层是兼容测试勾选几个常用的大型Mod确认没出现加载顺序相关的红字。第三层是跨版本测试在Mod的About.xml里明确标注支持的版本范围而不是一份XML用到底。进阶阶段值得做两个事一是把Mod拆成核心程序集 内容Defs两层核心程序集负责Harmony补丁和GameComponent内容Defs全部走Abstract和Patch机制。这样新增内容时不需要改C#代码出问题也能快速定位是逻辑层还是数据层。二是习惯用反编译工具阅读游戏程序集把它当作游戏源码来用Rimworld的大量行为细节不在文档里而在Assembly-CSharp.dll的IL里。最后说一个坚持了很久的习惯每次改完Mod发布前强制清空一次游戏日志、重进游戏、跑一遍触发路径确认日志里没有Unknown、Exception、Patch跳过这三类关键词。这个动作虽然机械但能挡掉绝大多数发布后红字。希望帮到你。本文还有配套的精品资源点击获取