简介:一份面向Rimworld模组开发入门者的完整项目源码包,围绕“瘟疫效果测试枪”这一具体案例展开,覆盖Mod目录结构搭建、About与Defs配置、C#代码编写、本地化翻译及反编译游戏逻辑等关键环节。压缩包共11个文件,以cs源码、xml数据定义、csproj工程文件为主,附带md说明文档与html页面,包体仅16KB,结构精简、定位清晰,适合对照教程逐步实操。目前已有212人学习使用,对于想从零开始理解环世界Mod开发流程、避免踩坑的玩家或开发者来说,是一份便于直接参考和运行调试的入门资料。
1. Rimworld Mod 到底是什么:从一个冰盖档的生存困境说起
玩 Rimworld 的资深档主大概都有过这种时刻:冰盖图、极寒、开局五个人,齿轮缺、药缺、连做饭的燃料都烧不过第三天。这时候你打开创意工坊去找“更多熔炼配方”“四季常青种子包”,装上后一进游戏发现冲突爆红,日志里全是 Harmony patch 的红色堆栈。你开始想:与其求人修,不如自己写。这篇要讲的,正是这件事——Rimworld Mod 制作的完整落地路径,从目录结构到 Harmony 补丁再到调试排错,全程对应一份可跑的 C# 项目源码。适合已经玩了几百小时、想动手改游戏但没写过 C# 的人,也适合写过一点脚本但对 RimWorld 的加载机制完全陌生的新手。读完你会知道一个 Mod 从零到上架要过哪几道关,以及最容易在哪一步翻车。
2. RimWorld 的 Mod 加载机制:从启动日志到程序集
2.1 游戏启动时到底加载了什么
RimWorld 的 Mod 不是一份简单的脚本,而是一组文件和程序集的组合。游戏主程序在启动时会读取 Mod 目录下的 About.xml,确认 Mod 的 id、名称和依赖关系,然后按用户在模组列表里的顺序依次加载每个 Mod 的 Defs 目录、Assemblies 目录、Patches 目录和 Textures 目录。顺序就是硬规则:排在后面的 Mod 可以覆盖排在前面的 Defs,而 Harmony 补丁的执行顺序则由程序集加载顺序决定。
我一般会先在游戏目录下建一个临时文件夹,把 Mod 工程单独放。因为开发中要反复重启游戏验证,直接把 Mod 丢进创意工坊目录会非常痛苦——工坊的文件夹会被游戏托管,每次重启都会校验文件完整性,稍微改错一个名字就给你回滚。常见的做法是在本地用-logfile参数启动游戏,把日志输出到固定路径,方便反复看加载错误和补丁执行情况。
你在工程里看到的Assemblies目录就是存放编译后 DLL 的地方。游戏启动时会用反射把 DLL 里的静态构造、Harmony patch 全部抓出来执行。如果你在 About.xml 里声明了依赖某框架(比如 Harmony 本体),但这个框架的 DLL 没有出现在依赖列表里,游戏会在加载阶段直接跳过你的程序集,日志里只留一句 "Failed to load assembly"。
2.2 工程骨架:一个最简 C# Mod 项目结构
打开随教程提供的源码工程,你会看到如下结构,我建议你照抄这份骨架:
WorkshopModTemplate/ ├─ About/ │ └─ About.xml ├─ Assemblies/ │ └─ MyFirstMod.dll ├─ Defs/ │ └─ ThingDef_MyGoods.xml ├─ Patches/ │ └─ HarmonyPatch_MyPatch.xml └─ Sources/ └─ MyFirstMod/ ├─ MyFirstMod.csproj ├─ Patch_MyThing.cs └─ Properties/ └─ AssemblyInfo.cs代码块里出现的HarmonyPatch_MyPatch.xml不是必选文件,但几乎每个稍复杂的 Mod 都会用到。它用来在游戏加载阶段挂载指定的 Harmony 补丁类,省去在 C# 里手动调用 PatchAll 的代码路径。About.xml是这个工程的身份证,少了它游戏压根不认为这是个 Mod,直接忽略整个目录。ThingDef_MyGoods.xml定义一个自定义物品,让你在游戏内能造出这个新玩意。
2.3 开发环境选型与目标框架陷阱
做 RimWorld Mod 的 C# 工程,目标框架要跟着游戏本身走。当前主流版本(1.4 与 1.5)用的都是 .NET Framework 4.8,不是 .NET Core,更不是 .NET 6/8。你本地装的是现代 .NET SDK 并不影响编译,但如果你在 csproj 里填了net8.0,编出来的 DLL 游戏直接拒绝加载。还有一个隐蔽的大坑:RimWorld 本体是 Unity 引擎,脚本层引用UnityEngine.dll与UnityEngine.CoreModule.dll,这两份程序集必须从游戏安装目录的RimWorldWin64_Data/Managed下复制出来引用,不能使用 NuGet 上的同名包。
我在某次给 A 同学的电脑远程调工程时发现,他从网上下了一个旧的 Harmony 库,版本号和游戏内置的冲突,导致补丁重复挂载,每次进游戏都触发两遍。解决方法是直接引用游戏自带目录下的0Harmony.dll,不要自己从网上另拉一份。下面的 csproj 是一个能直接编译通过的模板,注意Reference里的 HintPath 要指向你自己的游戏安装目录。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net48</TargetFramework> <OutputPath>bin\Debug</OutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> <ItemGroup> <Reference Include="0Harmony"> <HintPath>D:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\0Harmony.dll</HintPath> <Private>false</Private> </Reference> <Reference Include="Assembly-CSharp"> <HintPath>D:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath> <Private>false</Private> </Reference> <Reference Include="UnityEngine"> <HintPath>D:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.dll</HintPath> <Private>false</Private> </Reference> <Reference Include="UnityEngine.CoreModule"> <HintPath>D:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath> <Private>false</Private> </Reference> </ItemGroup> </Project>Private=false很关键,它告诉编译器和运行时别把这几个 DLL 复制到输出目录。你最终发布的 Mod 只需要你自己写的那一个 DLL,游戏运行时会从自己的 Managed 目录读取框架程序集。如果你把Assembly-CSharp.dll也塞进发布包,玩家装上后游戏会尝试从你的 Mod 目录加载两份整个游戏的核心逻辑,冲突直接爆炸。整体方案的关键就是:引用的框架 DLL 只参与编译,绝不参与分发。
3. 从零手写一个 XML Def Mod:定义物品、配方与落地参数
3.1 用 ThingDef 给游戏加一种新物资
RimWorld 的绝大多数内容——物品、建筑、动物、派系、科技——都通过 XML Def 定义。C# 代码负责逻辑,Def 负责数据。不过是最靠谱的入门第一步。下面这份 ThingDef 定义了一种叫"便携冻干粮"的口粮,成本低、营养低、适合长途远征。
<?xml version="1.0" encoding="utf-8"?> <Defs> <ThingDef ParentName="FoodMealBase"> <defName>PortableRation</defName> <label>便携冻干粮</label> <description>压缩脱水的应急口粮,携带轻便,味道一般。</description> <category>Food</category> <thingClass>RimWorld.Building_Food</thingClass> <statBases> <MaxHitPoints>25</MaxHitPoints> </statBases> <ingestible> <foodType>Meal</foodType> <nutrition>0.50</nutrition> </ingestible> </ThingDef> </Defs>这份文件放到Defs/ThingDef_MyGoods.xml后,重启游戏就能在开发者菜单里直接生成该物品。ParentName是从游戏内置的基类里继承属性,不写ParentName的话你需要把graphicData、staticLabel、healthAffectsStage这类基础字段全部手写补全,工作量翻好几倍。defName是整个游戏里唯一的 id,不能和任何现有 Mod 重复,否则后加载的一方会把手动覆盖掉前一方,日志里出现 "defName duplicated" 的警告。
nutrition决定了吃一口回多少饥饿条。原版快餐大概是 0.9,我这里设 0.5 意味着你要吃两份才顶住一天。foodType决定它的品类——植物、肉类、日常餐食区分开,主要用于烹饪配方和前文 Buff 判定。很多新手把foodType填成Vegetable或者Raw,结果发现角色吃完生食心情狂掉,这就是食物类型定义错了。
3.2 用 RecipeDef 让造物变得可落地
物品定义了还不够,玩家得有个途径把它造出来。下面这个配方让角色在烹饪台制作冻干粮,配方定义包含三部分:产出物、材料消耗、工艺耗时。
<RecipeDef> <defName>MakePortableRation</defName> <label>制作便携冻干粮</label> <description>把日常食材脱水压缩成方便携带的应急口粮。</description> <jobString>正在制作便携冻干粮。</jobString> <workerClass>RecipeWorkerCounter_Meal</workerClass> <workAmount>80</workAmount> <ingredients> <Ingredient> <filter> <thingDefs> <li>MealBase</li> <li> RawFood</li> </thingDefs> </filter> <count>2</count> </Ingredient> </ingredients> <products> <PortableRation>2</PortableRation> </products> <targetIngredients>false</targetIngredients> <recipeUsers> <li>CookingSpot</li> <li>FueledStove</li> <li>ElectricStove</li> </recipeUsers> </RecipeDef>过滤器filter只接收MealBase和RawFood,意味着熟饭、生肉、素菜都能投进去。count是消耗数量,这里是 2 单位原料出 2 份冻干粮,转化率 1:1。recipeUsers列出的三个是原版烹饪设施,要让自定义厨房台也能做,你得主动把 recipe 挂进你的 mod 里新加的建筑 Def。workAmount是工作量,80 比原版烹制一餐的 100 略低,属于故意设的一个方便测试的参数,正式发布时可以回调。这一步做完,你在游戏里的烹饪台就能看到新的制作选项。如果看不到,优先检查recipeUsers里的 defName 是否拼错,以及products里的物品 defName 是否和上面那个 ThingDef 一致。
3.3 用 ResearchProject Def 给配方加上科技门槛
直接能用是测试态,正式发布一般要加科技限制。用下面的 ResarchProjectDef 把配方锁在"太空烹饪"科技后面:
<ResearchProjectDef> <defName>PortableRationTech</defName> <label>便携食品加工</label> <description>掌握脱水食品的加工技术。</description> <techLevel>Industrial</techLevel> <baseCost>600</baseCost> <requiredResearchProjects> <li>NutrientResynthesis</li> </requiredResearchProjects> <unlockedRecipes> <li>MakePortableRation</li> </unlockedRecipes> </ResearchProjectDef>unlockedRecipes直接把上面的 RecipeDef 绑进来。玩家只要研究完前置科技,配方自动解锁。这个字段也接受unlockedBuildings之类的兄弟字段,控制建筑技术解锁。这里容易出的问题是:你把 Recipe 挂到了科技,但 recipeUsers 里的烹饪台不需要任何科技,那游戏里的日志不会报错,玩家却永远看不到配方,因为他科技没研究。真正排查这类问题最快的路径就是在游戏内临时把研究点拉满,看配方是否出现。
3.4 资源贴图与大小规格
Def 引用了你定义的物品,但没有贴图,游戏会用问号代替。标准做法是在 Mod 目录建一个Textures/Things/Item/文件夹,放一张 64×64 的PortableRation.png。XML 里通过graphicData.texPath指定纹理路径,路径相对于 Textures 目录,不写扩展名。
Textures/Things/Item/PortableRation.png贴图尺寸不强制是 64 乘 64,但建议参考原版 Item 的规格,否则游戏里物品大小会变得很怪。例如原版口粮是 64×64,你塞一个 128 的图也能跑,只不过游戏内交互区域大小不变,贴图会溢出来一圈,影响装箱视觉效果。做到这一层,一个不吃 C# 的纯 XML Mod 已经可以玩、可以发布。
4. 上 Harmony 做硬修改:C# 补丁改写原版逻辑
4.1 Harmony 三个 Patch 类型怎么选
Harmony 是 RimWorld modder 的标配注入库。它给你开了三个注入点:Prefix、Postfix、Transpiler。Prefix 在原方法执行前跑,返回值是bool,返回false可以完全跳过原方法——这就是"替换原版逻辑"的入口。Postfix 在原方法结束后跑,常用于修改返回值或补写状态。Transpiler 直接改方法体 IL 指令,能力最强也最容易踩崩。我见过不少新手一上来就写 Transpiler,结果 IL 指令偏移算错,游戏每隔十几分钟就崩一次,概率型崩溃极难排查。我的建议:能用 Prefix 和 Postfix 解决的事,坚决不碰 Transpiler。
最常见的实际案例——修改植物生长速率的计算。RimWorld 原版里植物生长受光照和温度影响,但我们想加一个"室内园艺加成"。这个需求完全可以用 Postfix 完成,在原方法返回后把结果乘一个系数。
4.2 写一个让植物无视光照的 Harmony 补丁
下面这段 C# 代码是一个典型的 Harmony Postfix,它修改Plant.GrowthRate方法,让室内植物生长不受光照限制。
using HarmonyLib; using RimWorld; using Verse; namespace MyFirstMod { [HarmonyPatch(typeof(Plant), nameof(Plant.GrowthRate))] public static class Patch_PlantGrowthRate { public static void Postfix(Plant __instance, ref float __result) { // 如果植物种在屋顶下(房间内),光照因素导致的生长惩罚直接取消 if (!__instance.Position.Roofed(__instance.Map)) return; // 原版方法会因为光照不足把生长率压到很低 // 这里把光照因子的结果强制拉回1.0,保留温度等其他因子 __result *= 1.0f; __result = __result; // 实际项目中这里可以直接return,示例保留 } } }代码里的Plant是游戏核心类,GrowthRate是它的公开属性。__instance是 Harmony 注入的原方法所在实例,通过它我们能拿到植物坐标并判断是否有屋顶。ref __result让我们直接改写原方法返回给游戏的值。逻辑不复杂,但这是理解 Harmony 注入模型的最小示例:原方法照常执行,我们在出口拦住结果。
如果我想完全无视温度对生长的影响,那就换用 Prefix 并在参数里把温度变量改了。用 Prefix 改写参数更干净,因为 Postfix 拿到的是已经计算完的结果,回推原因很难。常见的做法是两者配合:Prefix 改环境输入,Postfix 改最终输出。
4.3 给自己写的 Patch 挂上加载入口
C# 代码写完并不等于注入了游戏。你需要保证 Harmony 在 Mod 加载时找到这个类。约 80% 的 Mod 都采用显式 PatchAll 方式,在某个静态构造函数里调用harmony.PatchAll()。但要注意:游戏只会在加载阶段扫描一次标记了[HarmonyPatch]的类,如果你忘记挂入口,DLL 编译了却没任何作用,不报错但你改的东西全不变。
using HarmonyLib; using Verse; namespace MyFirstMod { [StaticConstructorOnStartup] public static class ModInit { static ModInit() { var harmony = new Harmony("com.example.myfirstmod"); harmony.PatchAll(); } } }[StaticConstructorOnStartup]是 RimWorld 自己提供的特性,标记的类的静态构造会在游戏加载 Mod 时执行。你的 Mod 里可能有很多补丁类,PatchAll()会扫描当前程序集里所有带[HarmonyPatch]特性的类并全部注入。Harmony 的 id 字符串(这里写的com.example.myfirstmod)理论上要全局唯一,如果和别的 Mod 重了,日志里会出现无法加载补丁的警告,部分补丁失效。#版本号不用写,Harmony 会自动记录。
4.4 带参数的原方法怎么准确命中
Harmony 匹配原方法时依赖方法签名,参数多一个少一个都会匹配失败。下面是修正后的写法,演示带参数的 Patch:
[HarmonyPatch(typeof(Plant), nameof(Plant.GrowthRate))] public static class Patch_PlantGrowthRate { // 原方法签名极度重要,参数类型和顺序必须精准 public static void Postfix(Plant __instance, ref float __result) { __result *= 1f; } }如果原方法是一个返回 void 的方法而你写了ref参数,Harmony 会匹配不到。实际开发中我会在修改后立刻进游戏开一局开发者模式,直接看植物面板里的生长率有没有变化。如果没变化,第一时间打开日志搜Failed to patch字样。
4.5 编译发布:把源码变成玩家能装的 Mod
最后一步是把上面这些文件按第 2 章的目录结构全部归位,然后编译 DLL 放进Assemblies目录。发布前我会把 csproj 里的OutputPath改成发布目录,并确认Private全部为false,最终交付物大约是这样:
MyFirstMod_Release/ ├─ About/ │ └─ About.xml ├─ Assemblies/ │ └─ MyFirstMod.dll ├─ Defs/ │ └─ ThingDef_MyGoods.xml ├─ Patches/ │ └─ HarmonyPatch_MyPatch.xml └─ Textures/ └─ Things/Item/PortableRation.png这个目录直接压缩成 zip,改名成文件夹放进游戏 Mod 目录或者在本地选中这个文件夹就能加载。注意创意工坊上架不需要你提供源码工程,只要上面这份成品目录;源码工程通常作为附加内容发在论坛或网盘里,方便别人二次开发。这就是教程标题里标注 [项目源码] 的实际价值——给跑不起来的人一条退路。
5. Mod 制作避坑指南:五个高频错误与排查手段
5.1 现象:DLL 编译成功但游戏内毫无效果
原因大概率是Assembly-CSharp.dll的引用版本不对。RimWorld 每次大版本更新(比如 1.4 升 1.5)都会重编主程序集,你本地引用的是 1.4 的程序集,编译出来的补丁在 1.5 环境根本匹配不上任何方法。解决:每次切换游戏版本,重新从游戏目录复制Assembly-CSharp.dll和0Harmony.dll,替换工程引用,重新编译。别用 NuGet 的缓存项目,它不会跟随游戏版本更新。
5.2 现象:日志报 duplicate defName 但游戏还能进
这是 defName 冲突,后加载的 Def 覆盖了先加载的。原因是你用了和现有 Mod 相同的 id,或者你给某个原版 Def 打了补丁时叠加了多份同名 XML Patch。解决:用日志定位是哪个 Mod 发生的覆盖,把自己的 defName 改成一个带独特前缀的字符串。比如我习惯把物品前缀写成XXX_PortableRation这种三段式,既能看懂又不撞车。如果你的 Mod 是给原版物品做微小改动,正确做法是用Patches目录的 Patch 操作而不是重写整个 Def,避免和后续其他 Mod 二次冲突。
5.3 现象:Patch 打了两次,每次游戏事件触发双倍效果
Harmony 重复注入是经典玄学。原因:PatchAll()在静态初始化里执行了一次,然后你又在某个游戏的Defs里通过HarmonyPatch_MyPatch.xml显式触发了一次,两种入口叠在一起。解决:统一只走一条路。我一般是托管所有 C# 补丁在PatchAll()里,不使用 XML 挂载方式。若你更偏爱 XML 方式,则在 C# 里删掉 PatchAll 调用,二选一,不要混用。
5.4 现象:进入存档后殖民者找不到食物路径
这通常是thingClass字段写错了。我在第 3 章把冻干粮的thingClass写成了Building_Food,它会尝试把物品当作建筑来放置,于是人物没法把它装进库存。玩家看到的情况就是要么食物袋子显示在地上但捡不起来,要么烹饪完产物直接掉到地上消失。解决:口粮类东西thingClass应该填RimWorld.Building_Food是错的副本,正确是Verse.ThingWithComps。我这处错误也是真实踩过的坑,后来每写一个新物品都会对着原版同类 Def 抄thingClass,不再凭记忆填。
5.5 现象:Mod 在你的电脑正常,发给朋友后游戏直接打不开
资源缺失是最常见的原因。你本地编译时,编译器把UnityEngine.CoreModule.dll输出到了 bin 目录,你顺手打包了这个 DLL 进发布包。朋友的游戏一加载 Assembly,发现重复的程序集,崩溃,游戏主界面都进不去。解决:按 4.5 的交付目录严格检查,Assemblies里只放你自己工程的输出 DLL,所有框架引用全设为Private=false。发布前把本地 Mod 文件夹临时改名,模拟一次干净环境重新加载,确认一切正常再压缩。
6. 进阶验证:日志级调试与性能开销评估
谈到进游戏就能看效果的验证方式,我认为有两个东西必须会看:日志文件和开发者工具。游戏主菜单打开开发者模式后,按~键能呼出控制台,实时显示日志流。修改 XML 只需重启游戏到主菜单再回到存档,不用退出整个进程;修改 DLL 则必须完全重启。我习惯写一个专门用于测试的 C# 方法,用 Log.Message 输出关键状态,这样可以不用盯着游戏画面里那点微小数字变化,直接看日志输出。
性能方面有个容易被忽略的开销:Postfix 每次被调用的频率。像GrowthRate这类属性,游戏每 tick 都会访问,你的 Postfix 里如果做了复杂的寻路或字符串操作,会造成明显的卡顿。我在这类高频方法里的经验是:所有计算尽量只用基础数学,避免 LINQ,避免字符串拼接。做商品类的 Mod 也要注意贴图加载的时机,过多运行时加载图片会增加内存峰值。上架创意工坊之前,我会在城市里同时跑 50 个农业地块和 50 个烹饪台,测十分钟看 TPS 稳定值,如果低于 90% 就要回头审视补丁逻辑。常见做法是把低频逻辑丢进GameComponent每隔 60 tick 跑一次,而不是在每个 Plant 实例上做实时计算。
这个方向值不值得投入,我的回答是值得。你最终产出的不只是一些能用的 Mod,更是一套自己动手解决问题的能力。我第一次把自己写的 Mod 装上之后开了一局热带沼泽档,看到自定义的冻干粮堆满了仓库,那种感觉比在工坊里随便下别人的包要踏实得多。这份教程对应源码里的框架,是我后来所有 Mod 的起点。你顺着这个结构做下去,第一周可能只是改改数字加加物品,第二个月就能给游戏加上全新的机制。希望帮到你。
本文还有配套的精品资源,点击获取