刚开始写RimWorld Mod的朋友,十个里有九个是被XML绊倒的。这句话不是我夸张,你去创意工坊评论区看,最常见的求助就是“按教程写了Defs,进游戏却报错”“加了矿石mod,贴图是紫黑色”“明明刚装好就和其他mod冲突,根本不知道改哪里”。RimWorld和很多游戏不一样,它的内容扩展大量依赖一套叫Defs的XML数据系统,小到一个物品、一种动物、一个科技,大到整条制作链,都可以不写一行C#就用Defs实现。而Defs的坑,主要集中在你以为它“只是键值对”的那部分:命名、结构、加载顺序,任何一个细节不对,游戏都能给你一个不知所云的报错。
这篇内容就是围绕这套东西写的。核心目标是帮你把Defs的命名规范和XML结构一次弄明白,再配合一份可以直接拷贝的实战代码,把从“写一个自定义物品”到“让它在游戏里正常出现”的完整路径走通。适合刚接触RimWorld Mod开发、还没写过C#、想先用XML做点东西出来的朋友,也适合写了一阵子但总在报错和冲突里打转的人对照排查。
1. Defs究竟是什么:先弄懂加载机制再动手
1.1 Defs是游戏的“数据资产库”,不是你随便写的配置文件
很多人第一次打开RimWorld的Core目录,看到满屏的XML会觉得这是配置文件。这么理解不算错,但会把思路带偏。Defs在游戏里的作用更像一个“数据资产库”:游戏启动时,程序会把所有XML里的Def对象读进内存,注册到DefDatabase里,之后所有系统——物品生成、任务生成、生态模拟、界面显示——都从这个数据库按名字查数据。
关键点在于,Defs和普通配置文件的哲学完全不同。普通配置项讲究“某个值错了只影响这个功能”,而Defs之间有很强的引用关系。你定义一个资源,需要在ThingDef里让它的stuffProps关联到材料类别,配方RecipeDef里要引用这个资源的defName,研究项目也要引用配方,链条一旦断了,游戏可能直接加载失败。所以第一步不是急着写节点,而是理解Defs只有一个入口:所有常规数据类型都挂在根节点<Defs>下,不同数据类型用不同的子节点名区分,比如<ThingDef>、<RecipeDef>、<ResearchProjectDef>、<HediffDef>。游戏拿到一个XML文件,先看根节点是什么类型,再把里面的子节点逐条注册成Def。
这个机制带来一个习惯性认知:写Defs时心里要有“注册”的思维。你不是在描述一个物品长什么样,而是在向游戏的“资产登记处”提交一份资产表。表里的字段名对应C#类里的属性名,节点层级对应类的嵌套结构。后面遇到Failed to load之类报错的时候,先想清楚是字段拼错了、层级错了,还是引用的其他Def不存在。
1.2 Mod的标准目录结构:文件放错位置等于白写
RimWorld加载Mod时有一套固定目录规范,不是说你随便塞个XML到任意文件夹就能被识别。我见过太多新手把Defs文件丢在Mod根目录,进游戏毫无反应,然后在评论区疯狂追问。正确做法是严格按照下面的目录结构放:
你的Mod文件夹/ ├── About/ │ ├── About.xml # Mod元数据,必填 │ └── Preview.png # 创意工坊预览图,可选 ├── Assemblies/ # C#编译出来的dll放这里,纯XML Mod可以没有 ├── Defs/ # 常规Defs XML文件放这里 ├── Patches/ # PatchOperation补丁XML放这里 ├── Languages/ # 本地化翻译文件 │ └── ChineseSimplified/ │ └── DefInjected/ ├── Textures/ # 贴图资源 │ └── Things/ │ └── Item/ │ └── Resource/ └── Source/ # 源码,通常不放发布包里游戏对目录的读取是分层级的:先处理About文件夹加载元数据,再按玩家在Mod列表里的顺序加载各个Mod的Defs、Patches、Assemblies。Textures不是“处理”而是“按路径查找”,因为Defs里的texPath只是字符串,里面有图片文件名但没有扩展名,游戏启动后渲染时再去Textures目录下找对应图片。这就是为什么贴图必须放在和texPath一致的相对路径下,而texPath不能带.png后缀。
很多新手会问:Patches文件夹和Defs文件夹有什么区别?简单说,Defs里的内容全部当成“新资产”注册;Patches里的内容是“对已有资产做手术”,通常用<Operation Class="PatchOperationAdd">这类节点修改其他Mod甚至原版的Def。虽然两者文件内容看起来都是XML,也可以都放在Defs下(系统按内容识别),但规范做法是分开,方便维护。
1.3 三种XML文件,根节点完全不同
准备写文件之前,先记住三种根节点。第一种是常规Defs文件,根节点是<Defs>,里面是各种Def;第二种是补丁文件,根节点也是<Defs>,但里面是<Operation>节点,系统会识别出来当作补丁执行;第三种是本地化文件,放在Languages目录下,根节点同样是<Defs>,但里面是“defName.字段名”格式的动态节点。第三种很容易和前两种搞混,因为同样是Defs根节点,语义完全不同。
这里有一个实际建议:刚开始写Mod时,先在本地建一个最小测试Mod,目录和About.xml都配好,然后放一个最简单的Defs文件进去,启动游戏确认能加载,再往里面加内容。这个“最小可用骨架”会帮你省掉大量的定位时间。我早期就是一次性写了十几个Defs然后一起进游戏,结果报错时报错信息根本分不清是哪个文件、哪个节点的问题。小步迭代,一次只加一个Def,这个工作流在后面会反复用到。
2. Defs命名规范:90%的冲突都出在这里
2.1 defName的硬性规则:这四条碰一条就报错
defName是一个Def的唯一标识,也是所有引用关系的“主键”。它在游戏里承担的任务非常重:物品生成要按它找,配方要按它找,存档要按它存,其他Mod的补丁也要按它定位。正因如此,它的命名规则非常严格。
第一条,只能由英文字母、数字、下划线组成。空格、连字符、中文、小数点都不允许。第二条,不能以数字开头。第三条,区分大小写。TitaniumIngot和titaniumingot是两个完全不同的defName,但游戏UI里显示什么取决于label字段,所以你不会第一时间发现区别,等存档读取时才暴雷。第四条,全局唯一。这里的“全局”指你安装的所有Mod加原版加DLC的完整Def数据库,不是你自己Mod内部唯一就行。
很多人只注意前两条,忽略了大小写问题。实际上RimWorld官方和社区默认的写法是PascalCase(每个单词首字母大写),比如ComponentIndustrial、MechanoidCluster。我建议你从第一天就统一用PascalCase,并且和C#侧的类型命名保持一致。如果后面写C#代码,DefDatabase<ThingDef>.GetNamed("TitaniumIngot")这样的字符串查找是大小写敏感且没有容错的,多写错一个字符就返回一个null,排查起来非常难受。
2.2 给你的defName加“姓”:前缀规范和团队协作
defName全局唯一这条规则,直接决定了你最好给defName加上表示Mod“姓氏”的前缀。原版物品叫Steel,你如果也定义一个Steel,游戏会加载两个同名Def,后加载的覆盖先加载的,你的东西可能从原版钢铁的图形变成你的图形,也可能反过来,谁先谁后完全取决于Mod排序,非常不可控。
社区通用做法是:前缀用Mod名或作者名的缩写,再跟下划线连接,比如我的示例Mod叫Titanium,那么所有defName都写成TitaniumIngot、TitaniumOre、MakeTitaniumIngot。如果Mod名很长,就用两到三个字母缩写,比如TMT_开头。这样即使多个Mod用了类似的名字,撞车概率也极小。
团队协作时,前缀更是“免责声明”。我见过几个为同一款整合包写扩展Mod的作者,因为彼此没提前约定,都定义了一个通用名字的中间资源,加载后互相覆盖,日志刷屏还没人发现,最后玩家存档直接坏档。后来我们约定文档里白纸黑字写明每个Mod的defName前缀、使用的缩写范围,后面再也没出过这类问题。哪怕是个人Mod,我也强烈建议建一个命名前缀记录表,一张图或一个txt都行。
2.3 defName、label、texPath三兄弟,各自管什么
关于命名,还有一个高频误区:以为defName就是显示名。实际上Defs里有三个“名称”各管一摊,理解清楚能避免很多后续麻烦。
defName管逻辑标识,只用于代码和引用,玩家基本看不到,一旦定下来尽量不要改,尤其是发过存档后,改了等于旧存档里所有该物品作废。label管显示,可以任意写成玩家能看懂的语言,比如钛合金锭,它对应游戏里的物品名。description管详细介绍,显示在物品信息栏。texPath管贴图路径,本身不是名称,但写法上总是被当成名称的一部分,它对应的是Textures目录下的相对路径。
这三个字段有一个常见坑:切换语言时,有些Mod显示空白名称,原因通常是只写了英文label,没提供本地化字段。RimWorld对语言的处理是:英文用XML里写的原生label,其他语言则优先读取Languages目录下的DefInjected翻译文件。如果你的Mod只写了英文label,切中文时游戏会直接显示空白,而不是回退到英文label。这个我放在后面第4章专门讲,这里先记住:label和本地化字段是两条线。
还有一点,很多Mod作者喜欢在用其他Mod的贴图时,把texPath直接写成别人的路径。这样能跑,但有版权风险和更新风险,对方一改路径你的贴图就紫黑。如果是自己的物品,建议还是单独放一份贴图在自己Mod的Textures目录下,路径用自己Mod内相对路径,哪怕只是复制粘贴一份图片。
3. XML结构详解:从零写一个资源类Def
3.1 一个最简ThingDef长什么样
下面这份代码是我用来演示的“钛合金锭”Defs,它是从零手写、不依赖任何原版抽象父节点的完整定义。你也可以在RimWorld原版安装目录的Core/Defs文件夹里看到类似写法。
<?xml version="1.0" encoding="utf-8"?> <Defs> <ThingDef> <defName>TitaniumIngot</defName> <label>钛合金锭</label> <description>一种高强度合金材料,可用于高级装备的制作。</description> <category>Item</category> <thingClass>ThingResource</thingClass> <graphicData> <texPath>Things/Item/Resource/TitaniumIngot</texPath> <graphicClass>Graphic_Single</graphicClass> <drawSize>0.65</drawSize> </graphicData> <stackLimit>75</stackLimit> <stuffProps> <categories> <li>Metallic</li> </categories> </stuffProps> <statBases> <MarketValue>120</MarketValue> <Mass>1.5</Mass> <DeteriorationSpeed>0.8</DeteriorationSpeed> <Flammability>0</Flammability> </statBases> </ThingDef> </Defs>先看前几行。<?xml version="1.0" encoding="utf-8"?>是XML声明,必须有,并且保存文件时建议用UTF-8无BOM编码。<Defs>是根节点,表示这是一个资产注册文件。<ThingDef>告诉游戏这一条注册的是物品类Def。defName、label、description不用多说,注意label我直接写了中文,这个在原生XML里没问题,但和本地化机制会是两套显示逻辑,后面细说。
category和thingClass是两个很容易被无视但其实很关键的字段。category决定物品在游戏体系里属于哪一个大类,常见取值有Item、Building、Pawn、Plant、Filth等,它会影响物品的交互方式、能否堆叠、能否被规则匹配。thingClass对应C#里的实际类名,这里用ThingResource表示它是一个可堆叠的资源类物品,而不是消耗品、武器或建筑。很多人做一个资源,结果items掉地上拾取不了,或者堆叠上限异常,多半就是thingClass选错了。
3.2 逐节点拆解:graphicData、statBases、stuffProps里到底在写什么
graphicData是物品外观的入口。最常见的坑是以为texPath就是图片路径,所以写成Things/Item/Resource/TitaniumIngot.png。这里不能带扩展名,因为它会被程序拼接处理。实际加载时,游戏会去Textures/Things/Item/Resource/目录找名为TitaniumIngot的贴图文件。graphicClass决定贴图以什么方式渲染,普通单张贴图用Graphic_Single;如果给植物或随时间变化外观的东西做动画,才会用到其他Graphic类。drawSize控制贴图在格子里的显示尺寸,0.65意味着比一格小一圈,这样堆在地面时不会显得太挤。
stackLimit表示一格最多堆叠多少。RimWorld有默认值,但资源类一般都要显式指定。不同资源差异很大,原版钢铁是75,木材是150,我这边的钛合金锭也设成75。数值本身不是越离谱越好,要考虑库存界面和运输效率的平衡。
statBases是物品的基础属性集合。这里的节点名对应游戏内的StatDef,MarketValue是市场价,影响交易价格和财富值;Mass是单件重量,虽然数字看起来不大,但如果一个仓库存几千个,总重量会非常恐怖;DeteriorationSpeed是户外风化速度,可以理解成物品在外头被日晒雨淋后的损坏速度;Flammability是易燃度,金属类设成0是合理的,数值越接近1越容易着火。这些属性只是整个StatDef体系里的一小部分,想知道有哪些可用,最靠谱的办法是打开原版的Defs文件夹搜索<statBases>节点。
stuffProps是这个资源能不能被当成材料使用的关键。很多金属资源做出来就是单纯物品,不能用于锻造装备,就是因为没有配置stuffProps。我这里的categories节点下用<li>列表形式指定它属于Metallic这个材料类别。<li>是列表元素的固定标识,凡是“一个Def里包含不确定数量的同类数据”的场景,基本都是用<li>包起来。有人第一次看到<li>以为这代表“列表”,没错,就是这个意思,但它必须放在正确的父节点下,层级写错一样报错。
3.3 配方RecipeDef:让资源真的能用起来
光有资源Def,充其量只是一个会出现在世界里的静态物品。要让玩家能在熔炼台把钛矿石冶炼成钛合金锭,还需要一个配方Def。看下面这份RecipeDef:
<?xml version="1.0" encoding="utf-8"?> <Defs> <RecipeDef> <defName>MakeTitaniumIngot</defName> <label>制作钛合金锭</label> <description>在锻造台上将钛矿石冶炼成钛合金锭。</description> <jobString>正在冶炼钛合金锭。</jobString> <workAmount>500</workAmount> <workSpeedStat>Stat_WorkSpeedGlobal</workSpeedStat> <workTable>TableSmelter</workTable> <researchPrerequisite>Smelting</researchPrerequisite> <ingredients> <li> <filter> <thingDefs> <li>TitaniumOre</li> </thingDefs> </filter> <count>5</count> </li> </ingredients> <products> <TitaniumIngot>1</TitaniumIngot> </products> </RecipeDef> </Defs>jobString是工作时左侧状态栏显示的文字,这个不写会导致工作时显示空白或一个奇怪的默认值。workAmount是完成这项工作所需的工作量,数字越大耗时越长,原版招募工作之类的大概几百到几千,500算是偏快,方便测试。workSpeedStat定义用什么属性来修正工作效率,Stat_WorkSpeedGlobal是通用工作速度。workTable指定在哪个工作台做,TableSmelter是原版熔炼台的defName,如果你把这里换成别的,配方就会出现在对应的工作台。researchPrerequisite是前置研究项目,Smelting是原版“冶炼”研究项目,没研究出来之前这个配方无法解锁。
ingredients结构比较特殊,它是一个列表(所以有<li>),每一项包含两个核心子节点:filter和count。filter是材料过滤器,指定这个配方接受哪些材料,里面可以写具体的thingDefs,也可以写categories(比如只要是金属材料都行)。count是要消耗的数量。我这里明确要求消耗5个TitaniumOre。
products的格式更特殊。它不是用<li>,而是直接以产物defName作为节点名,以产量作为节点文本,比如<TitaniumIngot>1</TitaniumIngot>。这种“以defName为节点名”的写法在RimWorld里并不少见,属于字典结构,含义是“产物→数量”的映射。很多新手第一次在这里卡住,就是因为不知道产物不是写在列表里,而是这样直接用Def名。
3.4 继承和补丁:什么时候用ParentName,什么时候写Patch
除了从零写Def,RimWorld还提供两套偷懒机制:继承和补丁。理解这两套机制,能让你少写大量重复代码,也避免踩到“定义冲突”的坑。
先看继承。原版Defs里有大量Abstract="True"的抽象Def,它们本身不会被注册成实际物品,而是作为模板给其他Def继承。最典型的写法是:
<ThingDef ParentName="BaseResource" Abstract="True">这行表示这个Def的字段从BaseResource那里继承。如果你写<ThingDef ParentName="BaseResource">(没写Abstract),就表示你定义了一个实际存在的Def,同时继承BaseResource的所有字段。这时你只需要覆写想改的字段,比如defName、label、graphicData、stackLimit,其他统计属性、thingClass、category都会从父节点带过来。
这种写法的好处是,原版团队已经把“资源类物品”共有的字段都整理好了,你不用每个资源都从零敲一遍。坏处是,你必须知道哪些字段被父节点定义了,否则可能出现“我想让这个物品不能堆叠,但父节点已经设了stackLimit 75,它继承过来了,我写的stackLimit 1却被另一个节点覆盖”这种纠纷。所以新手我更推荐先理解完整Def,再使用继承。
补丁机制则是完全不同的思路。继承是在写Def时取模板,补丁是在加载后对已有Def动手术。补丁的标准文件放在Patches文件夹,根节点还是<Defs>,但里面不是Def数据,而是<Operation>:
<?xml version="1.0" encoding="utf-8"?> <Defs> <Operation Class="PatchOperationAdd"> <xpath>/Defs/ThingDef[defName="TitaniumIngot"]/statBases</xpath> <value> <Beauty>10</Beauty> </value> </Operation> </Defs>这份补丁会在游戏加载所有Defs之后执行,用XPath定位到defName为TitaniumIngot的ThingDef的statBases节点,在里面加一个<Beauty>10</Beauty>子节点。XPath是这套机制的魂,/Defs/ThingDef[defName="..."]这种写法要熟练,因为后面你想修改任何Mod的属性,都是在和XPath打交道。注意,PatchOperationAdd是往目标节点里追加子节点,如果目标节点里已经有同名字段,追加会导致重复字段(游戏通常取最后一个或直接报错),所以想“改值”应该用PatchOperationSet字段,想“改名字”用PatchOperationReplace。
什么时候用继承,什么时候用补丁?一句话:自己的Mod内部做同类型物品,尽量用继承减少重复;跨Mod改别人定义好的东西,用补丁。补丁也是解决Mod兼容性问题的主要手段之一,因为它不需要改动对方的文件就能注入或替换内容,这也是RimWorld Mod生态能互相兼容的关键。
4. 实操:手把手把Mod装进游戏并验证
4.1 完整文件清单和放置路径
现在把前面两块拼起来。我建议你新建一个测试Mod文件夹,结构照着下面放:
TitaniumMod/ ├── About/ │ └── About.xml ├── Defs/ │ ├── TitaniumThingDefs.xml │ └── TitaniumRecipes.xml ├── Patches/ │ └── TitaniumPatches.xml ├── Languages/ │ └── ChineseSimplified/ │ └── DefInjected/ │ └── TitaniumDefs.xml └── Textures/ └── Things/ └── Item/ └── Resource/ ├── TitaniumIngot.png └── TitaniumOre.png和上一章的代码对应起来,ThingDefs里放TitaniumIngot和TitaniumOre两个物品Def,Recipes里放MakeTitaniumIngot配方,Patches里放那段给人造物品增加Beauty的补丁。把文件按这个结构放好,然后把整个TitaniumMod文件夹复制到RimWorld的Mods目录(游戏文件夹里通常有一个Mods文件夹,或者在游戏设置里能打开Mod文件夹)。如果是从Steam创意工坊下载的Mod,位置在Steam的workshop目录下,但你自己开发的本地Mod,放游戏Mods目录就行。
有一点要强调:Textures目录下的图片文件名必须与texPath匹配。TitaniumIngot.png对应texPath里的TitaniumIngot,TitaniumOre.png对应TitaniumOre。图片可以是你自己画的,也可以临时从原版资源里复制一张改名,只要像素格式正常(PNG或者JPG都行),游戏都能加载。
4.2 About.xml与版本兼容:老版本改法别乱套
Mod能不能被游戏识别,能不能正确匹配游戏版本,全看About.xml。下面这个是最小可用版本:
<?xml version="1.0" encoding="utf-8"?> <ModMetaData> <name>Titanium Mod</name> <author>YourName</author> <description>Adds titanium ore and ingots.</description> <supportedVersions> <li>1.5</li> </supportedVersions> </ModMetaData>name是Mod显示名,author是作者名,supportedVersions声明这个Mod支持哪些版本。很多新手直接把网上老教程的<version>或<targetVersion>抄过来,结果游戏不认。不同版本对元数据格式有调整,当前主流RimWorld 1.4和1.5都用<supportedVersions>,里面用<li>列出支持的版本号。如果你想同时支持1.4和1.5,就在里面加两行<li>。
description不是必填,但没有的话创意工坊页面和Mod列表里的描述就是空的,不太好看。这里顺便提醒一句:发布的Mod最好把<modDependencies>写清楚,声明依赖哪些前置Mod(比如Harmony、某个框架),否则玩家装了你依赖性质却不知道,运行时报错还会反过来骂你。本地测试时可以不加依赖,但发布前一定要补上。
4.3 DefInjected本地化:为什么你的mod不能直接显示中文
如果你在label里直接写中文,比如我上面写的<label>钛合金锭</label>,在中文环境下应该没问题。但问题是,如果你后续给自己的Mod增加了英文翻译,或者想发布到创意工坊让全球玩家用,直接写死中文就不合适了。正确做法是label写英文,然后用DefInjected给不同语言提供翻译。
DefInjected文件放在Languages/ChineseSimplified/DefInjected/目录下,内容长这样:
<?xml version="1.0" encoding="utf-8"?> <Defs> <TitaniumIngot.label>钛合金锭</TitaniumIngot.label> <TitaniumIngot.description>一种高强度合金材料,可用于高级装备的制作。</TitaniumIngot.description> </Defs>这里的关键就是根节点仍然是<Defs>,但子节点名是“defName.字段名”的格式。label对应label字段,description对应description。RimWorld在加载中文时会扫描Languages/ChineseSimplified目录,把这些翻译注入到对应Def里。如果这个文件缺失,游戏就用原XML里的英文label;如果连英文label也没有,才会显示空白。所以你会看到很多半吊子Mod只有英文或只有中文,本质上是没做全套。
有一个容易搞混的点:DefInjected文件的文件名可以随便起,游戏会扫描文件夹里所有XML,不必和Defs文件同名。但为了维护方便,还是建议和Defs文件保持对应。我已踩过这个坑,名字随便起的后果就是几个月后回来维护,根本分不清哪个文件对应哪个逻辑。
4.4 开发者模式验证:让游戏自己告诉你哪里写错了
启动游戏前,先确认你在游戏设置里开启了开发者模式。具体入口是游戏主菜单的“选项”里勾选“开发者模式”,勾好后游戏顶部多了一排小图标。这里有两个功能对Mod开发极其重要。
第一个是日志窗口。启动器或游戏内按Ctrl+Shift+L能打开日志,或者直接在游戏的安装目录下找到Player.log(不同系统位置不同),里面有完整的Def加载日志。所有关于“Failed to load”“Duplicate defName”的信息都会写在这里。第二个是开发者模式里“Write Defs to XML”功能,它会把当前加载后的完整Defs数据导出成一份XML,你可以在里面搜索自己的defName,看加载出来的实际字段长什么样。这招非常管用,很多你以为写了对、但实际被覆盖或没生效的字段,一搜便知。
我的标准流程是这样的:改完Defs → 启动游戏 → 看日志有没有红色报错 → 开发者模式直接搜索defName → 检查要的字段是否都在 → 感觉正常就生成一个物品或调出配方测试。一次只验证一个功能点,别急着一次加一堆。这个流程能帮你把“写Mod”从玄学变成工程。
5. 常见问题与排查实录
5.1 进游戏就报错的四类高频问题
我整理了一份高频报错速查表,都是我在群里看新人问过上百遍、自己早期也踩过的类型。
| 报错现场 | 常见原因 | 排查顺序 |
|---|---|---|
| 日志提示XML解析失败,指向某一行 | 尖括号没闭合、节点名拼写错误、字段名打错 | 先用编辑器格式化XML,再看报错行号 |
| 提示defName重复或覆盖 | 多个Def用了同样defName,后加载覆盖先加载 | 全局搜索defName出现几次,确认前缀 |
| 提示某个Def找不到(例如ParentName不存在) | 引用了一个不存在的抽象Def或defName拼错 | 检查原版是否真有这个Def,注意大小写 |
| 进入游戏后新建地图时崩溃 | 某个Def引用了不存在的研究、配方或工作台 | 顺着引用链逐个排查 |
第一种最常见,基本都是手工敲XML时手滑。第二种是命名规范问题,解决办法就是加前缀、全局限定。第三种和第四种往往是同一个根因:引用链断裂。比如RecipeDef里workTable写了TableSmelter,但你的游戏版本里这个Def改名了或不存在,游戏启动时不会立刻报错,到了生成工作台或任务时才炸。这时候别着急改代码,先打开原版Defs搜索一下对应defName是不是真的存在。
5.2 紫黑色贴图、空白名称、不显示配方,这类“不报错”问题
比报错更折磨人的是游戏正常运行,但效果不对。最常见的是物品贴图变成紫黑色,这是引擎找不到贴图的默认表现。原因一般是texPath和Textures目录对不上。注意几点:路径从Textures文件夹开始算,不带扩展名,大小写要和实际文件名一致。RimWorld是区分大小写的,titaniumingot.png和TitaniumIngot.png是不同的文件。另一个原因是图片格式不对,RimWorld能直接读取PNG和JPG,但某些工具保存的TGA或带特殊色彩通道的图可能出问题,优先用PNG。
空白名称通常和本地化有关。前面说过,中文环境下如果DefInjected文件缺失或节点名写错(例如TitaniumIngot.Label,大小写错了),游戏会显示空字符串。有些老Mod就是这样,中文界面下物品名空着,英文界面正常,就是没提供翻译。还有一种是label字段本身没写,那肯定空白,先把label补上。
配方不显示的情况,优先检查researchPrerequisite。如果配方依赖的研究还没完成,那是正常的,不是bug。排除这个后,再检查workTable是不是对的工作台。另外ingredients里如果写了不存在的资源Def,这个配方的显示也会被影响。我的经验是,配方这类逻辑链长的功能,出现问题先别猜,开发者模式下用“Debug Log”或者直接“Make”测试一下,很快能定位是显示问题还是逻辑问题。
5.3 排序和依赖:不是所有报错都怪你的代码
RimWorld的Mod加载顺序就是Mod列表里的顺序。后加载的Def会覆盖先加载的同名Def,后加载的补丁会修改先加载好的数据。所以同样一份Defs,在A排序下正常,在B排序下可能异常。这里有一个非常经典的坑:你的Mod定义了一个新资源,另一个Mod也定义了同名资源,谁排在后面谁就赢了。如果你的Mod依赖某个框架,比如Harmony,那必须在About.xml的<modDependencies>里声明,并且玩家需要把框架排在你的Mod前面。
排查排序问题有一个技巧:看日志里有没有关于defName覆盖的警告。RimWorld默认会记录“Def named X overrides previous def”,搜索这些关键字,能快速找到是谁和谁在打架。还有一种是“父Def覆盖”,某个Mod把原版的BaseResource抽象Def改了,导致所有继承它的资源都变了属性。这种很难一眼发现,但好在导出Defs的XML后能直接对比,我基本靠这个功能定位。
5.4 我的日常排查工作流和小工具
最后分享我现在惯用的工作流。文本编辑器我用VS Code,装一个XML格式化插件,每次写Defs都先格式化一遍,语法错误会明显少很多。打开RimWorld的日志文件用任何文本查看器都行,但我会先把日志文件目录固定到系统记事本快捷方式,方便快速打开。再配合开发者模式的“Write Defs to XML”,绝大多数问题都能在几分钟内定位。
遇到复杂问题,我会先做“最小复现”:把其他Mod全部禁用,只保留Core和我的Mod,看问题还在不在。如果在,大概率是我自己的逻辑或数据问题;如果不在,那就是兼容性问题,再逐个启用其他Mod做二分查找。这是所有Mod开发都能通用的排除思路。
还有一个小习惯:每次改完XML,我先在文本编辑器里搜一遍defName,确保没有重复,再进游戏。这条习惯帮我避开了至少一半的加载报错。另外,图片放进Textures之前先确认文件名和texPath一致,这个检查成本极低,收益极高。
我这几年写RimWorld Mod,最大的感受是:Defs这套东西,本质是“结构化数据”,难点从来不在语法本身,而在于理解和遵循游戏对数据的约定。命名规范是约定,目录结构是约定,节点层级是约定,连加载顺序也是一种约定。把约定摸清,后面写什么都是事半功倍。你也不用一次全记,先从今天这个钛合金Mod跑起来,再慢慢往里加功能,动手比死记文档有效得多。