如果你是一名《环世界》的资深玩家,或者正在尝试为这款游戏制作模组,那么你一定遇到过这样的困境:想要创造一个拥有独特地质风貌、矿物分布和地形逻辑的世界,却发现原版游戏的地形生成系统过于“黑盒”,难以深度定制。无论是想设计一个遍布巨型水晶矿脉的星球,还是想让特定矿物只在火山口附近生成,原版系统都显得力不从心。
这正是“地质学框架”这个模组试图解决的问题。它不是一个直接改变游戏内容的模组,而是一个为模组开发者提供的底层框架。简单来说,它把《环世界》中原本固定、分散的地形和矿物生成逻辑,拆解成了一套可编程、可组合的规则系统。开发者可以像搭积木一样,定义自己的“地质时代”、“岩石层”、“矿脉”和“侵蚀”规则,从而创造出逻辑自洽、细节丰富的独特世界。
本文将深入解析这个“地质学框架”的核心原理、安装配置方法,并通过一个完整的示例演示,手把手教你如何利用它来定制属于自己的《环世界》地质系统。无论你是想为你的大型模组增加独特的世界观基础,还是单纯想研究《环世界》模组开发的底层机制,这篇文章都将为你提供清晰的路径和可落地的代码。
1. 这篇文章真正要解决的问题:从“随机地形”到“可编程世界”
在深入代码之前,我们必须先理解这个框架解决的核心痛点。原版《环世界》的地形生成,对于玩家和大多数模组作者而言,是一个“结果”。你选择星球、点击生成,系统内部运行一套复杂的算法,最终给你一个随机但符合某些基础规则(如温度带、海拔)的世界。你想修改它,通常只能通过修改 XML 定义文件来调整一些权重和概率,比如让某种石头多一点,或者让某种地形出现的频率高一点。
这种修改是表层和结果导向的。你无法定义:“在海拔3000米以上、年降水量低于200mm的区域,只生成花岗岩,并且其中夹杂着稀有宝石矿脉”。原版系统没有提供这样的钩子(Hooks)。
“地质学框架”所做的,就是将地形生成的过程“管道化”和“规则化”。它引入了几个核心概念:
- 地质生成器:负责整个地形生成流程的调度。
- 岩石定义:不仅仅是石头的外观,还包括其硬度、肥沃度、作为地基的属性等。
- 矿脉定义:定义一种矿物如何生成,是均匀分布、矿脉状还是表层沉积。
- 地形处理器:在基础地形生成后,对其进行二次加工,如模拟侵蚀、河流切割、滑坡等。
通过组合这些元素,开发者可以精确控制一块地砖从无到有所经历的所有地质过程。这带来的直接价值是:
- 提升模组独特性:你的模组可以拥有独一无二的地质背景,成为世界观的重要组成部分。
- 增强游戏逻辑合理性:让资源分布、地形险阻与背景故事紧密结合。
- 降低大型模组开发复杂度:为地形相关功能提供一个统一、强大的底层支持,避免每个模组都自己造轮子。
接下来,我们将从零开始,搭建一个使用地质学框架的演示模组。
2. 核心概念与框架原理拆解
要使用这个框架,必须理解其核心的几个类和工作流程。我们可以将其类比为一个地质模拟流水线。
2.1 核心类与职责
GeologicalGenerator(地质生成器):- 角色:流水线总指挥。
- 职责:管理整个地形生成流程。它持有一个
WorldTile数据(代表地图上的一个格子),并按照配置顺序,调用一系列ITerrainProcessor(地形处理器)来逐步填充和修改这个格子的数据。 - 关键属性:一个处理器(Processors)的有序列表。
RockDef(岩石定义):- 角色:地基材料的配方。
- 职责:定义一种岩石的所有属性。这不仅仅是原版中的
TerrainDef(地形定义),而是更上层的抽象。一个RockDef会关联到具体的TerrainDef(用于渲染),同时定义其作为矿脉母岩的适宜性、开采难度、肥沃度修正等。 - 扩展性:你可以基于原版岩石(花岗岩、石灰岩等)创建新的
RockDef,也可以为模组新增的岩石创建。
VeinDef(矿脉定义):- 角色:矿物分布的蓝图。
- 职责:定义一种矿物(如铁、金、铀)如何在地图中生成。它决定了矿脉的形状(如网状、层状)、丰度、生成深度以及与特定
RockDef的关联性。 - 工作模式:框架提供了几种内置的矿脉生成算法(例如
VeinGenerator_Cluster用于生成矿簇),你也可以通过实现IVeinGenerator接口来自定义算法。
ITerrainProcessor(地形处理器接口):- 角色:流水线上的各个工位。
- 职责:这是框架扩展性的核心。每个处理器负责地形生成的一个环节。框架内置了一些处理器(如基础岩石生成、矿脉注入),你也可以创建自己的处理器来实现自定义逻辑(例如,根据魔法能量浓度生成魔法水晶矿)。
- 常见内置处理器:
Processor_BasicRock:根据海拔、湿度等基础参数,为地块分配一个基础的RockDef。Processor_Veins:根据已分配的RockDef和配置的VeinDef,在地块中注入矿脉。Processor_Erosion(示例):模拟风雨侵蚀对地形和岩石裸露程度的影响(需自定义实现)。
2.2 地形生成流程
一个典型的、使用了地质学框架的地形生成流程如下:
1. 游戏请求生成一个世界格子(WorldTile)的地形。 2. 地质生成器(GeologicalGenerator)被调用。 3. 生成器依次调用其处理器列表: a. Processor_BasicRock: 根据该格子的全局坐标、海拔、温度、降水等,从已定义的RockDef中选择一个作为该格子的“基岩”。 b. Processor_Veins: 检查所有VeinDef,如果该格子的条件(如岩石类型、海拔)符合某个矿脉的生成要求,则在此格子内“放置”该矿物。 c. Processor_Custom(自定义): 执行任何额外的处理,如添加特殊资源、修改肥沃度等。 4. 所有处理器执行完毕后,生成器将最终结果(主要是TerrainDef和资源信息)返回给游戏核心,用于渲染和游戏逻辑。这个流程的关键在于可插拔。你可以调整处理器的顺序,增加或移除处理器,从而完全改变地形生成的逻辑。
3. 环境准备与模组创建
在开始编码前,你需要准备好开发环境。
3.1 前置条件
- 游戏本体:《环世界》(RimWorld)最新稳定版。
- 开发环境:
- IDE:推荐使用 Visual Studio 2022 或 JetBrains Rider,它们对 C# 和 .NET 项目支持良好。
- .NET 框架:RimWorld 通常使用 .NET Framework 4.7.2 或类似版本,确保你的开发环境兼容。
- 必需模组:
- Harmony:几乎所有 RimWorld 模组的基石,用于实现代码补丁。
- 地质学框架(Geological Framework):本文的主角,你需要从 Steam 创意工坊或 GitHub 等渠道下载其源代码或已编译的 DLL 文件,以便在项目中引用。
- 模组项目结构:一个标准的 RimWorld 模组需要包含特定的文件夹和文件。
3.2 创建模组项目结构
在你的 RimWorld Mods 开发目录下(例如Steam\steamapps\common\RimWorld\Mods\),创建一个新的文件夹,命名为MyGeologyDemo。
在该文件夹内,创建以下结构和文件:
MyGeologyDemo/ ├── About/ │ └── About.xml # 模组元数据 ├── Assemblies/ │ └── (稍后放入编译的DLL) # 存放你编译的C#程序集 ├── Defs/ # 定义文件目录 │ ├── RockDefs/ # 岩石定义 │ ├── VeinDefs/ # 矿脉定义 │ └── GeologicalGenerators/ # 地质生成器定义 ├── Source/ # (可选)C#源代码目录 │ └── MyGeologyDemo.csproj └── 1.5/ # (可选)版本特定文件夹,用于1.5版本的内容3.3 编写 About.xml
这是模组的身份证。编辑About/About.xml文件:
<?xml version="1.0" encoding="utf-8"?> <ModMetaData> <name>My Geology Framework Demo</name> <author>YourName</author> <packageId>YourName.MyGeologyDemo</packageId> <!-- 必须全局唯一 --> <supportedVersions> <li>1.5</li> </supportedVersions> <description> 这是一个演示地质学框架(Geological Framework)用法的模组。 它添加了新的岩石类型和自定义的矿物生成规则。 </description> <modDependencies> <li> <packageId>brrainz.harmony</packageId> <displayName>Harmony</displayName> <steamWorkshopUrl>steam://url/CommunityFilePage/2009463077</steamWorkshopUrl> </li> <li> <!-- 假设地质学框架的packageId,你需要根据实际填写 --> <packageId>GeologicalFramework.Mod</packageId> <displayName>Geological Framework</displayName> </li> </modDependencies> </ModMetaData>4. 定义新的岩石类型
让我们首先创建一个新的岩石类型:“黑曜岩”(Obsidian)。它非常坚硬,但几乎无法支持植物生长。
在Defs/RockDefs/目录下创建文件RockDefs_Obsidian.xml:
<?xml version="1.0" encoding="utf-8"?> <Defs> <!-- 定义一种新的岩石 --> <GeologicalFramework.RockDef> <defName>Obsidian</defName> <!-- 内部标识符,必须唯一 --> <label>黑曜岩</label> <!-- 游戏中显示的名称 --> <description>一种由火山熔岩迅速冷却形成的黑色玻璃质岩石。极其坚硬,但土壤贫瘠。</description> <!-- 关联到原版或模组中的地形定义。这里我们关联到原版的“粗糙石地”,但可以自定义纹理 --> <terrainDef>RoughStone</terrainDef> <!-- 基础属性 --> <baseFertility>-0.8</baseFertility> <!-- 肥沃度极低 --> <pathCost>1.8</pathCost> <!-- 行走速度惩罚,值越大走得越慢 --> <workToMine>8000</workToMine> <!-- 开采所需工作量,原版花岗岩约为4000 --> <!-- 用于地质生成的权重和条件 --> <generationWeight>15</generationWeight> <!-- 在世界生成中的相对权重 --> <minElevation>0</minElevation> <!-- 最低生成海拔(单位:米) --> <maxElevation>1000</maxElevation> <!-- 最高生成海拔 --> <preferredTemperature>20</preferredTemperature> <!-- 偏好温度(摄氏度) --> <temperatureTolerance>40</temperatureTolerance> <!-- 温度容忍范围 --> <!-- 可以关联一个自定义的Texture2D路径,用于在世界地图上显示不同颜色 --> <!-- <worldTileTexPath>Terrain/ObsidianRock</worldTileTexPath> --> </GeologicalFramework.RockDef> </Defs>关键点解释:
defName是你在代码和其他定义中引用此岩石的唯一ID。terrainDef必须指向一个游戏中已存在的TerrainDef。如果你想有独特外观,需要先自定义一个TerrainDef。generationWeight是核心参数。当Processor_BasicRock运行时,它会根据所有有效RockDef的generationWeight以及当前格子的条件(海拔、温度)来计算概率,权重越高,在此条件下出现的几率越大。minElevation/maxElevation等条件用于过滤,不满足条件的岩石不会参与该格子的权重计算。
5. 定义自定义矿脉
接下来,我们定义一个只在“黑曜岩”中生成的稀有矿物:“虚空水晶”(Void Crystal)。
在Defs/VeinDefs/目录下创建文件VeinDefs_VoidCrystal.xml:
<?xml version="1.0" encoding="utf-8"?> <Defs> <GeologicalFramework.VeinDef> <defName>Vein_VoidCrystal</defName> <label>虚空水晶矿脉</label> <!-- 关联的游戏内物品(ThingDef)。你需要先定义这个物品,这里假设已存在 --> <thingDef>VoidCrystal</thingDef> <!-- 矿脉生成器类型。Cluster表示生成矿簇 --> <veinGeneratorClass>GeologicalFramework.VeinGenerator_Cluster</veinGeneratorClass> <!-- 矿脉生成器参数 --> <veinGeneratorParams> <clusterSize>5</clusterSize> <!-- 矿簇的平均大小(格子数) --> <abundance>0.003</abundance> <!-- 丰度,即每个格子有0.3%的几率属于此矿脉的一部分 --> <maxVeinsPerRock>2</maxVeinsPerRock> <!-- 同一种岩石上最多生成几个这种矿脉 --> </veinGeneratorParams> <!-- 生成条件 --> <allowedRocks> <li>Obsidian</li> <!-- 只允许在我们刚定义的黑曜岩中生成 --> </allowedRocks> <minElevation>500</minElevation> <!-- 只在海拔500米以上生成 --> <exclusive>true</exclusive> <!-- 独占性:如果此矿脉生成了,其他矿脉就不能在同一格子生成 --> </GeologicalFramework.VeinDef> </Defs>关键点解释:
thingDef必须指向一个有效的ThingDef,例如原版的Steel或你自定义的矿物物品。veinGeneratorClass决定了矿脉的形态。VeinGenerator_Cluster会生成一团一团的矿点,VeinGenerator_Sheet可能生成层状矿脉。你需要查阅框架文档来了解所有可用的生成器。abundance是一个非常敏感的参数。0.003 意味着平均每 333 个符合条件的格子中,大约有1个格子会有这种矿物。设置过高会导致世界布满矿物,失去稀缺性。allowedRocks是实现地质逻辑关联的关键。通过将VeinDef与特定的RockDef绑定,你可以模拟真实世界中特定矿物与特定岩性的共生关系。
6. 组装地质生成器
现在,我们需要创建一个新的GeologicalGenerator,将我们自定义的岩石和矿脉规则应用到世界中。
在Defs/GeologicalGenerators/目录下创建文件GeologicalGenerator_MyCustom.xml:
<?xml version="1.0" encoding="utf-8"?> <Defs> <GeologicalFramework.GeologicalGenerator> <defName>MyCustomGeology</defName> <label>我的自定义地质</label> <description>应用了黑曜岩和虚空水晶规则的自定义地质生成器。</description> <!-- 处理器执行顺序列表 --> <processors> <!-- 1. 基础岩石生成 --> <li Class="GeologicalFramework.Processor_BasicRock"> <!-- 可以在此处覆盖默认的岩石选择算法参数,例如温度、降水权重 --> </li> <!-- 2. 注入我们自定义的矿脉(以及框架/其他模组加载的所有其他矿脉) --> <li Class="GeologicalFramework.Processor_Veins"/> <!-- 3. (示例) 可以在这里添加一个自定义处理器,例如模拟侵蚀 --> <!-- <li Class="MyGeologyDemo.Processor_ObsidianWeathering"/> --> </processors> <!-- 指定这个生成器可以覆盖(替换)原版的哪个生成器。 通常,我们会替换掉原版默认的生成器。 --> <replaces>Vanilla</replaces> </GeologicalFramework.GeologicalGenerator> </Defs>关键点解释:
processors列表的顺序至关重要。必须先有岩石,才能在上面生成矿脉。replaces标签告诉框架,这个自定义生成器应该替换掉标识为Vanilla的原版生成逻辑。这样,当世界生成时,就会使用我们这套规则。- 你可以在处理器标签内添加子标签来传递参数给处理器,这需要你查阅对应处理器的源代码或文档。
7. 编写C#代码:一个自定义地形处理器(进阶)
如果你想实现更复杂的地质逻辑,比如让黑曜岩在高温地区有特殊效果,就需要编写 C# 代码,创建一个自定义的ITerrainProcessor。
首先,确保你的Assemblies文件夹下有地质学框架的 DLL 引用。然后在Source/目录下创建 C# 项目文件并编写代码。
创建一个新的 C# 类文件Processor_ObsidianWeathering.cs:
// 文件路径:MyGeologyDemo/Source/Processors/Processor_ObsidianWeathering.cs using GeologicalFramework; using RimWorld; using Verse; namespace MyGeologyDemo { // 实现 ITerrainProcessor 接口 public class Processor_ObsidianWeathering : ITerrainProcessor { // 处理器的唯一标识符 public string Id => "MyGeologyDemo_ObsidianWeathering"; // 在生成器中的执行顺序,数值越小越先执行(在矿脉生成之后) public int Order => 100; // 核心处理方法 public void Process(WorldTile tile, GeologicalGenerator generator) { // 1. 获取该格子当前的基础岩石 RockDef baseRock = tile.GetRock(); if (baseRock == null || baseRock.defName != "Obsidian") { return; // 如果不是黑曜岩,则不处理 } // 2. 获取该格子的温度等信息(这里简化处理,实际应从tile或地图数据获取) // 假设我们有一个方法从游戏世界中获取该格子的年平均温度 float annualAverageTemp = GetTileTemperature(tile); // 3. 自定义逻辑:如果温度极高,则“风化”黑曜岩,增加其肥沃度(模拟热液活动?) if (annualAverageTemp > 40f) // 40摄氏度以上 { // 这里我们无法直接修改RockDef,但可以影响最终的地形或添加特殊资源 // 例如,我们可以有一定概率在此格子添加一个“硫磺喷口”的物体 if (Rand.Value < 0.05f) // 5%几率 { // 假设我们有一个“Geyser”的ThingDef。这需要提前定义。 // tile.AddSpecialFeature("Geyser"); Log.Message($"[GeologyDemo] 在高温黑曜岩区域({tile.Tile})添加了地热特征。"); } // 或者,我们可以强制将此格子的地形改为更肥沃的变种(如果定义了的话) // tile.terrainDef = DefDatabase<TerrainDef>.GetNamed("RichSoil"); } // 4. 另一个逻辑:如果海拔很高且温度低,黑曜岩可能更坚硬(增加开采工作量) if (tile.elevation > 2000 && annualAverageTemp < -10f) { // 可以通过Harmony Patch或其他方式,在开采时动态增加工作量。 // 这里只是演示,在实际处理器中,可能需要将信息存储在tile的CustomData中供后续系统读取。 tile.CustomData["MiningWorkMultiplier"] = 1.5f; } } // 辅助方法:模拟获取格子温度(实际开发中需调用游戏API) private float GetTileTemperature(WorldTile tile) { // 这是一个简化示例。真实情况下应使用Find.WorldGrid[tile.Tile].temperature 或类似方法。 // 这里返回一个随机值用于演示。 return Rand.Range(-20f, 50f); } } }编写完此代码后,你需要编译成 DLL,放入Assemblies/文件夹。然后,你需要在之前的地质生成器定义中启用这个处理器:
<!-- 在 GeologicalGenerator_MyCustom.xml 的 processors 列表中添加 --> <li Class="MyGeologyDemo.Processor_ObsidianWeathering"/>8. 测试与验证你的模组
- 编译与放置:将你的 C# 项目编译为
MyGeologyDemo.dll,放入Assemblies/文件夹。确保所有 XML 定义文件就位。 - 激活模组:启动 RimWorld,在模组列表中启用 “Harmony”、“Geological Framework” 和 “My Geology Framework Demo”。注意加载顺序,框架模组应在你的演示模组之前加载。
- 创建新世界:
- 进入世界创建界面。
- 在通常的星球参数设置下方或旁边,地质学框架可能会添加一个下拉菜单,用于选择地质生成器。
- 选择 “我的自定义地质”(
MyCustomGeology)。
- 生成与检查:
- 生成世界后,使用开发者模式(Dev Mode)快速检查。
- 打开“调试操作菜单”(Debug Actions Menu),寻找与地形或资源相关的工具。
- 使用“在地图上放置物品”工具,尝试放置你定义的
VoidCrystal,看物品是否存在。 - 更直接的方法是,使用“查看地图信息”工具,点击地图上的岩石地块,查看其
TerrainDef和RockDef信息,确认是否是Obsidian。
- 验证矿脉生成:这需要一些运气。你可以在世界生成后,使用开发者模式的“重新生成当前地图”功能(风险较高),或者开一个新存档,在包含黑曜岩的高海拔区域仔细寻找。也可以临时调高
VeinDef中的abundance值以便测试。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模组加载失败,红字错误 | 1. XML 语法错误。 2. 依赖模组未加载或顺序错误。 3. DLL 引用缺失或版本不匹配。 | 1. 查看游戏启动日志(Player.log)。 2. 检查红字错误的具体行号和错误信息。 3. 确认 About.xml中的packageId和依赖项正确。 | 1. 使用 XML 验证工具检查文件。 2. 调整模组加载顺序,确保框架在前。 3. 确保 Assemblies文件夹内有正确版本的 DLL。 |
| 自定义岩石/矿脉在游戏中不显示 | 1.defName拼写错误或重复。2. XML 文件未被加载(路径错误)。 3. 生成条件(海拔、温度)过于苛刻,没有符合条件的格子。 4. 地质生成器未正确应用。 | 1. 使用开发者模式“数据库查看器”搜索你的defName。2. 检查游戏日志是否有加载该 Def 的记录。 3. 临时放宽生成条件(如去掉海拔限制)测试。 4. 在世界创建界面确认选择了正确的地质生成器。 | 1. 确保所有defName唯一且引用一致。2. 确保 XML 文件在 Defs/的正确子文件夹下。3. 调整 generationWeight和生成条件参数。4. 检查地质生成器 Def 中的 replaces标签。 |
| 自定义处理器未生效 | 1. 处理器类未正确实现ITerrainProcessor。2. 处理器未添加到地质生成器的 processors列表。3. DLL 未成功加载或包含错误。 | 1. 在处理器代码开头添加Log.Message输出,看是否执行。2. 检查地质生成器 XML 中处理器的类名(含命名空间)是否正确。 3. 查看日志中是否有关于程序集加载的错误。 | 1. 确保类实现了接口的所有成员(Id,Order,Process)。2. 确保 XML 中的 Class属性是完整的命名空间+类名。3. 重新编译 DLL,确保引用了正确版本的框架 DLL。 |
| 游戏性能下降或生成速度变慢 | 1. 处理器逻辑过于复杂或循环低效。 2. 矿脉 abundance设置过高,导致计算量激增。3. 自定义岩石/矿脉数量太多。 | 1. 使用性能分析工具定位瓶颈。 2. 简化 Process方法中的逻辑,避免频繁的查找和计算。3. 检查是否在处理器中进行了不必要的全局查找。 | 1. 优化算法,缓存常用数据。 2. 将 abundance设置在合理范围(通常远小于0.01)。3. 考虑将一些计算从每格处理移到世界生成前或后的一次性处理。 |
10. 最佳实践与工程建议
- 始于设计,而非代码:在动手写 XML 和 C# 之前,先用文档或草图规划好你的地质系统。你想模拟什么样的地质过程?岩石和矿物之间有何关联?这能帮你设计出更合理的
RockDef和VeinDef。 - 保持兼容性:
- 你的
RockDef可以关联原版的TerrainDef,这样即使玩家没有安装你的模组,地图也不会出现缺失纹理的“紫块”。 - 在自定义处理器中,对空引用(null)进行严格检查,避免因其他模组改动导致崩溃。
- 谨慎使用
replaces。如果你只是想添加内容,而不是完全替换原版逻辑,可以考虑创建全新的生成器,让玩家选择。
- 你的
- 参数化与可调:将关键数值(如权重、丰度、条件阈值)作为
ModSettings(模组设置)暴露给玩家。这能极大提升模组的可玩性和兼容性。 - 善用日志输出:在开发阶段,在关键节点使用
Log.Message或Log.Warning输出信息,这是排查问题最有效的手段。发布前记得移除或禁用这些调试日志。 - 性能优先:世界生成会调用你的处理器成千上万次(每个格子一次)。确保
Process方法尽可能高效。避免在循环内进行昂贵的操作(如DefDatabase查找、复杂的数学运算),必要时可预先计算好查找表。 - 与其他模组协作:地质学框架是一个底层框架。考虑你的岩石和矿物如何与“烹饪”、“冶炼”、“建筑”等其他系统交互。例如,你可以为你新增的岩石定义独特的建材属性,或为你新增的矿物定义特殊的冶炼配方。
通过地质学框架,你获得的不再是简单修改几个数字的能力,而是创造一整套地质演化规则的权力。它要求开发者从“结果修改者”转变为“规则设计者”,这无疑提高了门槛,但也为《环世界》的模组生态打开了通往更复杂、更真实模拟世界的大门。从定义一种岩石开始,逐步构建起它的生成条件、伴生矿物、风化特性,最终形成一个自洽的地质系统,这个过程本身就是对逻辑设计和系统思维的一次绝佳锻炼。
建议将本文的示例代码作为起点,先成功运行一个最简单的自定义岩石模组,然后尝试添加一种与之关联的矿物,最后再挑战编写一个自定义处理器来实现更动态的效果。每一步都进行测试和验证,确保你完全理解每个参数和每一行代码所产生的影响。