尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

RimWorld Mod开发:从XML到Harmony补丁的工程实践

发布时间:2026/9/29 22:51:36

资讯中心
01
ARTICLE

RimWorld Mod开发:从XML到Harmony补丁的工程实践

RimWorld Mod开发:从XML到Harmony补丁的工程实践
简介这是一份面向 Rimworld 模组开发入门者的 C# 项目源码配套逐步制作「瘟疫测试枪」的完整流程覆盖 Mod 目录结构About、Defs、Assemblies 等、开发工具选用、数据定义、本地化翻译与游戏代码反编译思路。源码包共 11 个文件压缩后仅 16KB以 cs 代码文件与 xml 数据定义文件为主包含 csproj 工程配置、Languages 本地化配置、markdown 说明及页面展示文件结构与官方模组规范保持一致便于对照修改与二次开发。作者从搭建工程、编写 C# 代码、反编译校验到测试验证均有涉猎配合注释与说明文档可快速还原完整开发链路适合有一定 C# 基础、希望快速上手 Rimworld Mod 开发的玩家与开发者。截至目前已有 207 人学习下载是一份小而精的入门实践包。1. 为什么我建议直接啃 Rimworld Mod 制作源码包先分清 XML、C# 与 Harmony 的边界我常被问到一个问题想做 Rimworld Mod是不是只改 XML 就够了答案取决于你想做到哪一层。给小人加个新心情、新物品、新配方XML 完全能应付但一旦要改游戏行为——比如让“夜晚还在户外干活的小人心情更好”这种需求就必须写 C# 并给游戏方法打 Harmony 补丁。这份项目源码正好把两种姿势放进同一个工程里有可直接编译的 C# 项目、配套的 Defs 与 About 文件、一个最小可用的 Harmony 补丁示例全部按 RimWorld 1.5 的 API 写。适合已经装过 Mod、想做出第一个“有逻辑”的 Mod 的人也适合老手直接抄一套稳定工程骨架省去自己搭参考链接的弯路。2. 一个 RimWorld Mod 的运行骨架About、Defs、程序集与加载顺序2.1 最小可用目录三层文件各管什么先把最要紧的事说清楚RimWorld 的 Mod 本质就是一个普通文件夹Steam 创意工坊把文件夹打包发布本地测试则直接把文件夹丢进游戏主目录下的 Mods 目录。一个能跑的最小工程长这样StarGaze/ ├─ About/ │ └─ About.xml ├─ Defs/ │ └─ ThoughtDefs.xml ├─ Assemblies/ │ └─ StarGaze.dll游戏加载时按三层读取About.xml 只负责给主菜单读显示 Mod 名称、作者、版本和 packageId写错了顶多不显示不影响逻辑Defs 里的 XML 会被注入游戏的 DefDatabase成为游戏数据的一部分Assemblies 里的 DLL 则是在游戏启动时被程序集加载器捞起来执行的。三个层面按固定顺序加载互相依赖。很多新手把全部精力放在写 XML 上结果 C# 类里写了个游戏中不存在的 DefName游戏不会当场报错只在日志里轻轻留一句“Could not load type”排查起来很费劲。源码包把这三层都对齐了照着抄就行。还有一个细节游戏内置的 Mod 管理器会校验 packageId 的格式通常写成作者名.mod名比如yourname.stargaze。同一台机器上 packageId 不能重复否则两个 Mod 会互相覆盖。准备在创意工坊发布的话packageId 最好一次定死后面改会引发订阅用户大量报错。2.2 C# 与 XML 的职责划分哪些事必须上代码纯 XML 能做的事很广但有一个明显边界凡是要读取运行时状态、要做条件判断、要遍历地图对象的XML 都做不到。我常用下面这张表跟人讲选型需求场景只用 XML 够不够是否需要写 C#新增一种心情 Thought固定加几点心情够否这个 Thought 要“只在夜间户外出现”不够必须写 ThoughtWorker给小人加一把新武器够通常否给武器加“对机械族额外伤”的机制不够需要 StatWorker / Harmony改某个原版物品的堆叠上限够否Override 一行让原版某个工作逻辑失效不够必须 Harmony核心判断标准就一句如果需求里出现“当……时”“如果……就”这类动态逻辑那就是 C# 的活儿。源码包里的示例“星际瞭望员”触发条件是“夜晚 户外 小人存在”这三个条件全部依赖运行时数据所以必须写ThoughtWorker子类。XML 只负责声明这个 Thought 叫什么名字、加几点心情、显示什么文案。新手最容易搞反的是把 XML 当脚本语言用试图用li写分支逻辑。RimWorld 的 Defs 是数据声明不是代码它没有“如果”语法。遇到这种需求请回到 C#。2.3 StaticConstructorOnStartup 与 HarmonyMod 代码的两个入口C# 代码写好后游戏怎么知道你写了什么靠两个入口。第一个是[StaticConstructorOnStartup]这是 Verse 命名空间提供的特性。带这个特性的静态构造函数会在游戏启动、Defs 全部加载完毕之后自动执行最适合做补丁注册和全局初始化。第二个入口是 Harmony 补丁本身。Harmony 是 RimWorld 官方集成的一个补丁库版本 2.x游戏主目录的 Managed 文件夹里就带着 0Harmony.dll不需要额外分发。补丁类型常用的有 Prefix、Postfix、Transpiler 三个Prefix 在目标方法执行前运行可以改参数或拦掉原方法Postfix 在目标方法执行后运行适合读取结果做附加逻辑Transpiler 直接改 IL 指令效率最高但也最难看懂。绝大多数 Mod 场景用 Prefix 和 Postfix 就够。给游戏方法打补丁的注册方式有两种一种是手写harmony.Patch(...)另一种是PatchAll()自动扫描程序集里所有带[HarmonyPatch]特性的类。源码包用的是后者因为省事新加补丁类不需要一行一行手动注册using HarmonyLib; using Verse; namespace StarGaze { [StaticConstructorOnStartup] public static class HarmonyInit { static HarmonyInit() { var harmony new Harmony(com.yourname.stargaze); harmony.PatchAll(); } } }参数说明new Harmony(...)的字符串是补丁的唯一 ID建议用反域名格式避免和别的 Mod 撞车。PatchAll()无参自动从当前程序集找所有[HarmonyPatch]类并应用。整个类的[StaticConstructorOnStartup]保证这段代码一定在游戏启动时执行不需要玩家做任何操作。2.4 调试环境从 IDE 到 RimWorld 日志的闭环RimWorld 的日志分两处游戏内开发者模式下按~键呼出的控制台以及 Unity 生成的 Player.log 文件。Windows 上路径通常是%LOCALAPPDATA%\Low\Ludeon Studios\RimWorld\Player.logLinux 和 macOS 在~/.config/unity3d/Ludeon Studios/RimWorld/Player.log。调试 Mod 最常用的命令是直接开一个终端实时看日志# Windows PowerShell Get-Content $env:LOCALAPPDATA\Low\Ludeon Studios\RimWorld\Player.log -Wait -Tail 50 # Linux / macOS tail -f ~/.config/unity3d/Ludeon Studios/RimWorld/Player.log-Wait和-f都是保持监听状态日志一有新输出立刻显示-Tail 50表示只从末尾 50 行开始跟避免旧日志干扰。加载 Mod 时先开日志再启动游戏看到红字第一时间能定位到具体类名和方法名比在游戏内控制台里翻快得多。游戏崩溃时日志不一定紧跟报错Unity 经常在崩溃前多写一段“Loading completed”之类的内容。所以查看日志时别只盯最后几行多往上翻 3050 行往往能发现真正的根因这个习惯帮我省过无数次排查时间。3. 源码包实战把「星际瞭望员」Mod 从零编译到跑起来3.1 工程骨架与 csproj 引用配置RimWorld 1.5 基于 Unity 2022 和 .NET Framework 4.8所以 C# 项目必须把目标框架设为 net48。源码包的 csproj 我建议直接照抄只改项目名和路径这样可以避开最常见的“引用版本不对”问题Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknet48/TargetFramework LangVersion9.0/LangVersion AppendTargetFrameworkToOutputPathfalse/AppendTargetFrameworkToOutputPath Deterministictrue/Deterministic /PropertyGroup ItemGroup Reference IncludeAssembly-CSharp HintPathE:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll/HintPath Privatefalse/Private /Reference Reference IncludeUnityEngine HintPathE:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.dll/HintPath Privatefalse/Private /Reference Reference IncludeUnityEngine.CoreModule HintPathE:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll/HintPath Privatefalse/Private /Reference Reference Include0Harmony HintPathE:\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\0Harmony.dll/HintPath Privatefalse/Private /Reference /ItemGroup /Project关键参数就三个TargetFramework定死 net48这决定你只能在旧语法范围内写代码别用record之类的现代 C# 新特性AppendTargetFrameworkToOutputPath设为 false可以让编译产物直接输出到bin\Release而不是bin\Release\net48后面拷贝省一步每个Reference的Private必须设为 false否则 IDE 会把游戏 DLL 复制到输出目录导致 Mod 包体积暴增还会让你误以为 DLL 已经随包发布。HintPath是 Windows 上的默认 Steam 路径如果你的游戏装在其他盘改成自己的实际路径即可。注意 Assembly-CSharp.dll 是游戏主逻辑程序集所有 RimWorld 游戏类型都在这里查 API、写类型引用全靠它。3.2 写 About 和 Defs声明一个心情增益About.xml 是游戏主菜单读取的门面文件内容不多但 packageId 和 supportedVersions 必须写对?xml version1.0 encodingutf-8? ModMetaData nameStarGaze - 星际瞭望员/name authoryourname/author packageIdyourname.stargaze/packageId supportedVersions li1.5/li /supportedVersions description夜晚户外工作者获得“星光抚慰”心情增益。/description /ModMetaDatasupportedVersions写 1.5表示只兼容当前版本。如果还想兼容 1.4可以多加一个li1.4/li但如果你用了 1.5 新增的 API千万不要这么干否则玩家在旧版本加载后直接红字。源码包的 Defs 文件建在Defs目录文件名随意游戏会自动扫描整个目录下的 XML。接下来是 ThoughtDef?xml version1.0 encodingutf-8? Defs ThoughtDef defNameStarGaze_NightOutdoor/defName workerClassStarGaze.ThoughtWorker_StarGaze/workerClass thoughtClassThought_Situational/thoughtClass stages li label星光抚慰/label description在夜晚的星空下工作心情安静而稳定。/description baseMoodEffect6/baseMoodEffect /li /stages stackingModeStack/stackingMode /ThoughtDef /Defs注意workerClass必须写完整格式“命名空间.类名”且对应类要继承ThoughtWorker。thoughtClass选Thought_Situational表示这是一个“情景触发型”心情不是记忆型——也就是说小人不一定非要记住这件事当前状态满足就生效。baseMoodEffect为 6表示心情加成 6 点。stackingMode用 Stack允许重复叠加多个小人同时触发也各自计算。3.3 写 C# 逻辑ThoughtWorker 和日志补丁核心判断逻辑写在 ThoughtWorker 子类里using RimWorld; using Verse; namespace StarGaze { public class ThoughtWorker_StarGaze : ThoughtWorker { protected override ThoughtState CurrentStateInternal(Pawn p) { if (p null || !p.Spawned || p.Map null) return false; bool isNight p.Map.skyManager.CurSkyGlow 0.1f; bool outdoors !p.Position.Roofed(p.Map); if (isNight outdoors) return true; return false; } } }三个判断的先后顺序有讲究先做空引用和存活检查再读地图数据因为CurSkyGlow和Roofed都依赖有效的地图实例。CurSkyGlow是天空管理器给出的当前光照强度夜间一般在 0.05 以下0.1 这个阈值是我实测后留出的余量可以防止清晨和傍晚临界帧抖动导致短期闪现。判断“户外”用的是!p.Position.Roofed(p.Map)这个方式比检查地形更准确——洞穴里没有屋顶也算室内游戏里叫“Roofed”这个语义是 RimWorld 特有的。源码包里还带了一个非常实用的调试补丁给游戏的Log.Error方法挂 Prefix把报错实时追加到独立文件using System; using HarmonyLib; using Verse; namespace StarGaze { [HarmonyPatch(typeof(Log), nameof(Log.Error))] public static class Patch_LogError { private static readonly string LogPath System.IO.Path.Combine( System.IO.Path.GetTempPath(), StarGaze_errors.log); public static void Prefix(string text) { if (string.IsNullOrEmpty(text)) return; try { System.IO.File.AppendAllText(LogPath, $[{DateTime.Now:HH:mm:ss}] {text}{Environment.NewLine}); } catch { // 日志写入失败不应影响游戏主流程 } } } }这个补丁的Prefix会在任何游戏代码调用Log.Error时抢先把错误文本写进系统临时目录下的独立文件。为什么要这么做因为 RimWorld 的 Player.log 是全量日志玩家装了十几个 Mod 后谁都能往里面写真正的元凶被淹没在几千行红字里。给错误写独立文件等于给自己装了个专属黑匣子。注意Path.GetTempPath()是系统级临时目录所有 Mod 共用所以文件名带StarGaze_前缀避免和其他 Mod 冲突。3.4 编译、部署与验证运行以上文件就位后在项目根目录执行构建dotnet build -c Release-c Release会输出优化后的 DLL排查问题时可以用 Debug 版获得更详细的调试信息。构建完成后把产物拷到 Mod 目录# Windows PowerShell $target E:\Steam\steamapps\common\RimWorld\Mods\StarGaze\Assemblies New-Item -ItemType Directory -Force -Path $target Copy-Item bin\Release\StarGaze.dll $target # Linux / macOS mkdir -p ~/RimWorld/Mods/StarGaze/Assemblies cp bin/Release/StarGaze.dll ~/RimWorld/Mods/StarGaze/Assemblies/启动游戏前先开日志监听然后进游戏主菜单确认 Mod 列表里出现了 StarGaze。进任意存档把视角拉到户外找到正在夜间干活的小人打开他的心情面板能看到“星光抚慰”并显示 6。如果没出现先看日志有没有红字再检查小人是否真的在无屋顶区域。用开发者模式的“超时快进”跑一晚上确认白天这个 Thought 会消失、夜晚重现这就是一个可以落地的 Mod 效果。如果你平时习惯让 AI 辅助生成代码这里有个忠告AI 生成的 Mod 代码往往混着 1.3、1.4 的 API最常见的翻车点是skyManager的属性和CompPriorityWork这类旧接口。拿它生成后一定先 CtrlShiftF 全局搜索 API 名再对照游戏安装目录的 Assembly-CSharp.dll 确认字段是否存在不要直接编译就部署。4. 避坑/排查日志静默、加载顺序与版本陷阱4.1 现象编译通过进游戏马上红字但日志只显示一行原因最常见的是workerClass或thoughtClass写错比如类名少写命名空间前缀游戏在 DefDatabase 初始化时加载这个类失败但异常消息很短只会提示类型无法加载。解决先在 XML 里检查所有类名字段是否带完整命名空间再打开dotnet build的编译输出确认 DLL 里类的完整名称。最笨也最可靠的办法是把 C# 类名和 XML 字段名抄在纸上逐个对照这类问题九成是大小写或拼写差异。4.2 现象Def 能加载但 C# 方法从未执行也没报错原因游戏通过反射加载了ThoughtWorker子类但你的判断逻辑里返回条件永远不成立或者Roofed的判断和你预期的物理环境不对应。比如你把小人放在“遮阳棚”下这种结构在地图数据里算有屋顶Roofed返回 true条件被拦掉。解决先在CurrentStateInternal的入口加入临时调试日志Log.Message($[StarGaze] Pawn {p?.LabelShort}, spawn{p?.Spawned}, glow{p?.Map?.skyManager?.CurSkyGlow});跑一次游戏看日志输出确认每个变量的实际值。如果压根没有这行输出说明workerClass挂错了类如果有输出但数值和预期不符说明是条件逻辑的问题。调试上这种“没报错却静默不工作”的玄学十次里有八次是条件逻辑问题。4.3 现象单独加载没问题和别的 Mod 一起加载就崩原因RimWorld 对未声明依赖的 Mod 是按名字装排列顺序加载的。你的 Mod 用了另一个 Mod 的 Def但没有在 About.xml 里声明modDependencies对方后加载时你的代码就找不到资源。还有一类是重复声明同一个 DefName两个 Mod 都定义了StarGaze_NightOutdoor后者直接覆盖前者。解决在 About.xml 里加依赖声明modDependencies li packageIdCore/packageId displayNameCore/displayName /li /modDependenciesCore 是 RimWorld 自带的基础 Mod写它可以让排序器把你的 Mod 排在 Core 之后。如果是第三方依赖把 packageId 换成对方的真名。重复 DefName 的问题只能用独立前缀根治比如所有 defName 都带StarGaze_前缀。4.4 现象游戏更新后旧存档一读就红字Mod 列表里显示版本不兼容原因RimWorld 1.5 改了一部分 API 签名你用的Roofed方法或某个字段在 1.5 里可能改路径了。游戏把不兼容的 Mod 直接灰掉避免强制加载。但如果你用supportedVersions写了 1.4 和 1.5而代码只按 1.5 编译1.4 玩家就会红字。解决发布前严格限定版本号只写自己测试过的版本不要写“向上兼容”。游戏大版本更新后重新编译一轮再发布。还有一个好习惯发布包里保留About.xml的supportedVersions注释每次更新前先去官方补丁说明里搜一遍你要的 API 是否变动。4.5 现象AI 生成的补丁代码编译时提示找不到方法或字段原因大语言模型在生成游戏 Mod 代码时经常混入旧版 API例如把 1.4 的Verse.Pawn_HealthTracker用法带到 1.5或者把某个内部方法名写错。编译器报错还算好的最坑的是 API 确实存在但行为改了编译通过但逻辑出错。解决把 AI 生成代码当成“参考骨架”所有 API 调用都要拿“引用 DLL 里的签名”核对一遍。我一般会用ilspy打开 Assembly-CSharp.dll 快速搜方法签名确认参数列表和返回类型完全一致再编译。对 Harmony 补丁方向Prefix/Postfix不确信时先用一个临时 Postfix 输出日志验证原方法确实被执行了再改正式逻辑。5. 发布前的最后一道验证用 Irony Mod Manager 做排序与日志回放源码包里的工程验证只是第一步真正要上创意工坊前我强烈建议走一遍 Irony Mod Manager 的流程。它是 RimWorld 社区常用的 Mod 管理工具可以用来做依赖排序、加载日志回放和 Mod 冲突预览比游戏自带的排序器直观得多。安装后把刚才的 Mod 文件夹拷给 Irony让它扫出来。重点做三个动作一是让 Irony 按依赖关系自动排序确认你的 Mod 排在 Core 之后二是启用“完整日志回放”启动游戏前勾选记录全量日志三是跑一个存档 30 天的速度测试专查长时间运行后是否出现内存泄漏或重复报错。跑完后打开 Irony 的日志面板搜索“Exception”和“StarGaze”两个关键词如果你自己的补丁没引发任何新增异常就算通过。最后检查清单可以这样核检查项标准About.xml 的 supportedVersions只写测试过的版本packageId已定死且不与他人重复XML 内所有类名带完整命名空间拼写一致DLL 引用Privatefalse无多余依赖日志中 Exception无新增异常卸载后旧存档能回滚加载不残留数据从那以后我每次发布新 Mod 都强制走一遍这套流程先本地跑、再 Irony 全日志回放、最后开一个旧存档验证兼容性三关过了才敢点击发布。遇到玩家报错时我第一件事也是要他的 Player.log配合我自己的黑匣子日志大部分问题十分钟内就能定位。做 Mod 这事前期把工程骨架搭正、把日志系统留好后面能省下大量靠猜的时间。希望这次整理的这套流程能帮到你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。