
1. 项目概述与核心价值如果你玩过《边缘世界》RimWorld并且对游戏里那些依赖天气、动不动就罢工的太阳能板感到又爱又恨那你可能已经动过自己动手改一改的念头。这个项目就是带你从零开始用C#代码亲手“复刻”一个属于你自己的太阳能发电机Mod。这不仅仅是复制一个游戏内物品而是深入理解RimWorld Mod开发的核心逻辑从动态发电计算、环境遮挡检测到游戏UI的绘制完整走一遍一个功能型建筑Mod的开发全流程。我之所以选择太阳能发电机作为切入点是因为它麻雀虽小五脏俱全。它涉及了RimWorld Mod开发的几个关键层面Thing物品/建筑定义、Comp组件逻辑、GameCondition游戏条件交互以及Gizmo命令工具的绘制。通过实现它你能系统地掌握如何让一段C#代码“活”在游戏世界里响应昼夜交替、天气变化并与玩家进行交互。网上很多教程只讲“怎么做”但踩坑的经验往往一笔带过。这篇指南会聚焦在那些容易出错的“坑点”上比如如何正确处理Tick游戏刻更新以避免性能问题如何精准计算遮挡率以及如何让自定义的Gizmo命令工具既美观又实用。最终你会得到一套可以直接编译、运行并且能在此基础上自由扩展的完整源码。2. 开发环境准备与项目结构搭建2.1 工具链选择与配置工欲善其事必先利其器。RimWorld Mod开发主要依赖以下工具Visual Studio 2022社区版完全免费对C#和.NET开发支持最好。确保安装“.NET 桌面开发”和“使用C的桌面开发”工作负载。RimWorld游戏本体这是我们的“运行时环境”和API来源。建议使用Steam版本便于管理。RimWorld Mod开发环境本质上我们是创建一个符合RimWorld Mod结构的C#类库项目。你需要引用游戏目录下的核心程序集主要是Assembly-CSharp.dll位于RimWorld\RimWorldWin64_Data\Managed\。这里有个大坑不要直接引用游戏目录下的DLL文件。因为游戏更新时这些文件会被覆盖。正确的做法是在Visual Studio中创建一个新的“类库.NET Framework”项目目标框架版本选择与游戏匹配的例如RimWorld 1.4通常对应.NET Framework 4.7.2。然后将这些必要的DLL文件如Assembly-CSharp.dll,UnityEngine.CoreModule.dll等复制到你的项目目录下一个单独的Libs文件夹中再从那里添加引用。这样你的项目依赖就与游戏本体解耦了。2.2 项目结构与Mod清单一个标准的RimWorld Mod目录结构如下MySolarGeneratorMod/ ├── About/ │ └── About.xml # Mod元数据名称、描述、作者等 ├── Assemblies/ │ └── MySolarGeneratorMod.dll # 我们编译的C#程序集 ├── Defs/ # XML定义文件 │ ├── ThingDefs/ # 物品/建筑定义 │ ├── ResearchDefs/ # 研究项目定义如果需要 │ └── ... ├── Languages/ # 多语言支持 │ └── ChineseSimplified/ │ └── Keyed/ │ └── Strings.xml ├── Textures/ # 纹理图片 │ └── Buildings/ │ └── SolarGenerator.png └── Source/ # 可选C#源代码目录方便分享About.xml是Mod的身份证其内容至关重要?xml version1.0 encodingutf-8? ModMetaData name自定义太阳能发电机/name author你的名字/author packageIdYourName.ModName/packageId !-- 必须全局唯一通常用作者.模组名格式 -- supportedVersions li1.4/li /supportedVersions description一个复刻并增强了逻辑的太阳能发电机。/description loadAfter liUnlimitedHugs.HugsLib/li !-- 如果依赖其他Mod在此声明 -- /loadAfter /ModMetaData避坑点1packageId的格式。它不能包含空格最好使用点号分隔且在整个Steam创意工坊中应保持唯一否则会导致Mod冲突或无法订阅。3. 核心逻辑设计与C#类结构3.1 理解RimWorld的能源系统与Tick机制在RimWorld中电力是一个核心资源。发电建筑如风力涡轮机、地热发电机通过一个名为CompPowerPlant的组件Component来工作。我们的太阳能发电机也将继承这个体系。但太阳能有其特殊性它的输出不是恒定的而是随时间昼夜和空间遮挡变化的。这里涉及RimWorld一个基础概念Tick。游戏世界每秒进行60次逻辑更新即60 TPS。大部分逻辑代码如我们的发电计算会在每个Tick或每若干个Tick被执行一次。直接在Tick()方法里进行复杂的计算如遍历所有地图格子检查遮挡是性能灾难。我们必须采用缓存和分帧计算的策略。3.2 设计自定义组件CompPowerPlantSolar我们将创建一个新的组件类CompPowerPlantSolar继承自CompPowerPlant。这是实现动态发电逻辑的核心。using RimWorld; using Verse; namespace MySolarGeneratorMod { public class CompPowerPlantSolar : CompPowerPlant { // 基础属性最大理论发电量瓦 public float MaxOutputWatts 1700f; // 当前输出效率0.0 - 1.0受昼夜和遮挡影响 private float curEfficiency 0f; // 遮挡率0.0 - 1.01.0表示完全被遮挡 private float roofCoverage 0f; // 缓存上一次计算遮挡的时间戳Tick数避免每帧计算 private int lastCoverageCheckTick -99999; // 遮挡检查间隔Tick数例如每250 Tick检查一次约4.17秒 private const int CoverageCheckInterval 250; // 核心属性当前实际发电量 最大输出 * 当前效率 public override float DesiredPowerOutput { get { // 如果父建筑太阳能板被损坏或未通电则不发电 if (parent.Map null || parent.Destroyed || !parent.Spawned || !FlickUtility.WantsToBeOn(parent)) { return 0f; } return MaxOutputWatts * curEfficiency; } } // 每Tick更新的逻辑 public override void CompTick() { base.CompTick(); // 调用父类基础逻辑 UpdateEfficiency(); } // 更新效率的核心方法 private void UpdateEfficiency() { // 1. 计算昼夜效率基于游戏时间 float dayNightEfficiency CalculateDayNightEfficiency(); // 2. 定期更新遮挡率非每Tick if (Find.TickManager.TicksGame lastCoverageCheckTick CoverageCheckInterval) { UpdateRoofCoverage(); lastCoverageCheckTick Find.TickManager.TicksGame; } // 3. 综合计算最终效率昼夜效率 * (1 - 遮挡率) // 假设遮挡物如屋顶、山体会完全阻挡阳光 curEfficiency dayNightEfficiency * (1f - roofCoverage); // 4. 通知电网更新功率重要 if (powerComp ! null) { powerComp.PowerOutput DesiredPowerOutput; } } // 计算基于游戏内时间的效率正弦曲线模拟日出日落 private float CalculateDayNightEfficiency() { // 获取当前地图的游戏时间管理器 Map map parent.Map; if (map null) return 0f; // GenLocalDate.DayPercent 返回当前游戏日内的时间进度0.0午夜0.5正午 float dayPercent GenLocalDate.DayPercent(map); // 使用平滑的曲线模拟太阳高度角正午效率最高1.0午夜为0 // 这里使用正弦函数的一个区间并限制在0-1之间 // 将一天的时间0-1映射到正弦函数的 -π/2 到 3π/2 区间取sin值并归一化 float rawEfficiency Mathf.Sin(dayPercent * Mathf.PI * 2f - Mathf.PI / 2f); // 将sin值从[-1, 1]映射到[0, 1] rawEfficiency (rawEfficiency 1f) / 2f; // 进一步平滑避免日出日落时效率变化过于突兀 rawEfficiency Mathf.SmoothStep(0f, 1f, rawEfficiency); // 考虑天气影响雨天、雾天降低效率 if (map.weatherManager.RainRate 0.1f) { rawEfficiency * 0.3f; // 雨天效率降至30% } else if (map.weatherManager.FogRate 0.5f) { rawEfficiency * 0.7f; // 浓雾效率降至70% } return Mathf.Clamp01(rawEfficiency); } // 更新屋顶/山体遮挡率 private void UpdateRoofCoverage() { if (parent.Map null) return; // 获取太阳能板占据的格子假设是3x3的建筑 CellRect occupiedRect parent.OccupiedRect(); int totalCells occupiedRect.Area; int roofedCells 0; // 遍历所有被占据的格子检查是否有屋顶或 overhead mountain头顶山体 foreach (IntVec3 cell in occupiedRect) { RoofDef roof cell.GetRoof(parent.Map); if (roof ! null (roof.isThickRoof || roof RoofDefOf.RoofRockThick)) { // 厚屋顶或岩顶视为完全遮挡 roofedCells; } // 你也可以检查其他遮挡物比如高大的建筑但这需要更复杂的射线检测或区域查询 } // 计算遮挡率 roofCoverage (float)roofedCells / totalCells; } } }避坑点2性能优化与Tick间隔。在UpdateRoofCoverage()方法中我们遍历了建筑占据的所有格子。对于一个3x3的建筑这是9个格子每250 Tick检查一次开销可以接受。但如果你的建筑很大比如5x5或者你在CompTick()中每帧都检查性能就会急剧下降。务必根据实际情况调整CoverageCheckInterval。对于静态遮挡如山脉检查间隔甚至可以更长如每2000 Tick。避坑点3电网更新时机。注意在UpdateEfficiency()最后我们手动设置了powerComp.PowerOutput。这是因为DesiredPowerOutput是一个属性其值的变化不会自动通知电网组件。你必须显式地更新powerComp.PowerOutput电网才会重新计算并分配电力。忘记这一步是导致“发电建筑显示有输出但电网没电”的常见原因。4. XML定义与游戏内实体绑定4.1 定义建筑ThingDefC#代码定义了行为但要在游戏里“造”出这个建筑还需要XML定义。在Defs/ThingDefs/下创建Buildings_Power.xml。?xml version1.0 encodingutf-8? Defs ThingDef ParentNameBuildingBase defNameSolarGeneratorCustom/defName label自定义太阳能板/label description一种先进的光伏板能更高效地将阳光转化为电能。其输出随昼夜和天气动态变化并受遮挡影响。/description graphicData texPathBuildings/SolarGenerator/texPath !-- 指向Textures/Buildings/SolarGenerator.png -- graphicClassGraphic_Single/graphicClass drawSize(3,3)/drawSize !-- 假设是3x3的建筑 -- /graphicData thingClassBuilding/thingClass categoryBuilding/category tickerTypeNormal/tickerType !-- 使用Normal Tick每帧更新 -- size(3,3)/size pathCost14/pathCost statBases MaxHitPoints150/MaxHitPoints Flammability0.5/Flammability WorkToBuild3000/WorkToBuild MarketValue800/MarketValue /statBases costList Steel150/Steel Component4/Component Glass40/Glass !-- 自定义资源需在OtherDefs中定义 -- /costList researchPrerequisites liSolarPanels/li !-- 需要先研究原版“太阳能板”科技 -- /researchPrerequisites !-- 电力相关组件 -- comps !-- 1. 电力生产者组件链接到我们的C#类 -- li ClassMySolarGeneratorMod.CompPowerPlantSolar compClassMySolarGeneratorMod.CompPowerPlantSolar/compClass basePowerConsumption0/basePowerConsumption !-- 注意这里不直接设置basePowerConsumption因为输出是动态的由C#代码控制 -- /li !-- 2. 可开关组件Flickable -- li ClassCompFlickable/ !-- 3. 可拆解组件如果需要 -- li ClassCompProperties_Self拆解 workTo拆解Factor0.5/workTo拆解Factor /li /comps !-- 放置规则需要开阔空间不能被屋顶覆盖 -- placementRules liPlacementHelper_NoRoof/li /placementRules designationCategoryPower/designationCategory inspectorTabs liITab_Power/li !-- 在电力标签页显示 -- /inspectorTabs /ThingDef /Defs避坑点4compClass与Class属性。在comps列表中Class属性指向的是组件属性类CompProperties的完整命名空间和类名而compClass是CompProperties内部的一个字段指向实际的组件逻辑类CompPowerPlantSolar。有时你会看到只使用Class属性并在C#的CompProperties派生类中指定compClass。上述写法是一种清晰且常见的混合写法确保XML能正确找到你的C#逻辑。4.2 添加自定义Gizmo命令工具为了让玩家直观地看到当前发电效率和遮挡情况我们需要添加一个自定义的Gizmo。这需要在我们的CompPowerPlantSolar类中重写CompGetGizmosExtra方法。using System.Collections.Generic; using RimWorld; using UnityEngine; using Verse; namespace MySolarGeneratorMod { public class CompPowerPlantSolar : CompPowerPlant { // ... 之前的代码 ... // 重写此方法以添加自定义Gizmo public override IEnumerableGizmo CompGetGizmosExtra() { // 首先返回基类的Gizmo如开关按钮 foreach (Gizmo gizmo in base.CompGetGizmosExtra()) { yield return gizmo; } // 添加我们自定义的信息显示Gizmo yield return new Command_Action { defaultLabel 太阳能板状态, defaultDesc $当前发电效率: {curEfficiency:P1}\n遮挡率: {roofCoverage:P1}\n输出功率: {DesiredPowerOutput:F0} W, icon ContentFinderTexture2D.Get(UI/Commands/SolarInfo), // 需要准备一个图标 action delegate { // 点击后可以显示更详细的信息这里简单打个日志 Log.Message($太阳能板状态详情 - 位置: {parent.Position}, 效率: {curEfficiency:P1}); }, // 禁用点击效果仅作为信息显示 Disabled true, // 即使禁用也显示并自定义禁用时的颜色和提示 disabledReason 此为状态显示器, }; } } }避坑点5Gizmo的性能与生成频率。CompGetGizmosExtra在玩家选中建筑时会被频繁调用。确保其中的逻辑轻量不要进行复杂的计算或数据库查询。所有动态数据如curEfficiency都应该在CompTick或类似更新循环中预先计算好Gizmo只负责读取和显示。5. 纹理制作、本地化与测试5.1 创建与导入纹理游戏中的建筑需要视觉表现。你需要为你的太阳能板制作纹理图片。基本要求格式PNG或DDS支持透明通道。尺寸必须是2的幂次方如 256x256, 512x512。对于3x3格的建筑游戏会将其平铺显示因此你的纹理应该设计成能无缝衔接的样式。路径按照项目结构将最终图片例如SolarGenerator.png放入Textures/Buildings/目录下。制作技巧可以参考原版游戏的纹理位于RimWorld\Mods\Core\Textures\Things\Building\Power使用像Aseprite、Photoshop或免费的GIMP等工具进行绘制。保持与原版一致的美术风格低多边形、略带手绘感会让你的Mod更协调。5.2 实现多语言本地化为了让Mod支持中文等其他语言需要在Languages/ChineseSimplified/Keyed/Strings.xml中添加翻译?xml version1.0 encodingutf-8? LanguageData !-- ThingDefs -- SolarGeneratorCustom.label自定义太阳能板/SolarGeneratorCustom.label SolarGeneratorCustom.description一种先进的光伏板能更高效地将阳光转化为电能。其输出随昼夜和天气动态变化并受遮挡影响。/SolarGeneratorCustom.description !-- Gizmo 和 UI 文本 -- MySolarGeneratorMod.SolarGenerator.StatusGizmoLabel太阳能板状态/MySolarGeneratorMod.SolarGenerator.StatusGizmoLabel MySolarGeneratorMod.SolarGenerator.EfficiencyTip效率{0}/MySolarGeneratorMod.SolarGenerator.EfficiencyTip !-- 研究项目如果添加了新的 -- ResearchProject_SolarGeneratorCustom.label高级光伏技术/ResearchProject_SolarGeneratorCustom.label /LanguageData在C#代码中使用Keyed翻译而不是硬编码字符串// 不好的做法 defaultLabel 太阳能板状态; // 好的做法 defaultLabel MySolarGeneratorMod.SolarGenerator.StatusGizmoLabel.Translate();避坑点6翻译键Key的命名空间。为了避免与其它Mod冲突翻译键最好以你的Mod的包ID或一个独特前缀开头如MySolarGeneratorMod.。5.3 在游戏中测试与调试编译与部署在Visual Studio中编译项目为Release模式将生成的MySolarGeneratorMod.dll复制到Mod目录的Assemblies/文件夹下。激活Mod启动RimWorld在Mod配置界面激活你的Mod。确保加载顺序正确如果依赖HugsLib等库需将其放在你的Mod之前加载。使用开发模式在游戏设置中开启“开发模式”。这将允许你即时建造按CtrlF12打开开发者工具使用“Spawn”菜单直接放置你的太阳能板。查看调试信息选中建筑观察自定义Gizmo是否正常显示信息是否正确。测试逻辑使用“时间控制”加速游戏观察昼夜交替时发电量的平滑变化。用开发工具放置屋顶测试遮挡检测是否生效。日志排查所有Log.Message、Log.Warning、Log.Error的输出都可以在游戏目录的Player.log文件中找到或在开发模式控制台查看。这是排查空引用异常NullReferenceException、逻辑错误的最重要工具。避坑点7版本兼容性与热重载。RimWorld版本更新如从1.3到1.4可能会修改API导致Mod失效。在About.xml中准确声明supportedVersions。在开发过程中修改C#代码后需要重启游戏才能生效。修改XML或纹理文件有时可以通过游戏内的“重新加载所有Mod”功能开发模式生效但并非总是可靠重启是最稳妥的方式。6. 进阶优化与功能扩展6.1 性能深度优化我们的基础实现已经考虑了Tick间隔但还有优化空间按需更新只有当太阳能板在屏幕上或附近有活动时才进行高频率的更新如每Tick计算昼夜效率。当它远离玩家视野时可以大幅降低更新频率如每1000 Tick一次。这可以通过重写CompTickRare每250 Tick调用和CompTickLong每2000 Tick调用来实现并根据与玩家摄像机的距离动态切换。遮挡计算优化UpdateRoofCoverage中的遍历对于大型建筑或大量建筑仍是负担。可以考虑使用Map.areaManager.Home或自定义区域来标记“需要检查遮挡”的区域只在区域变化时重新计算。将遮挡率计算分摊到多帧完成每帧只计算一部分格子。对于静态遮挡山脉在建筑放置时计算一次并缓存除非周围地形被改变如挖掘、坍塌。6.2 添加可研究升级让太阳能板可以通过研究解锁升级功能例如储能涂层研究后太阳能板在白天效率略降但能在夜间提供少量基础电力如最大输出的10%。自动清洁系统减少沙尘暴、火山灰对发电效率的影响。双轴追踪通过研究使太阳能板能缓慢追踪太阳提升白天的平均发电效率。实现方式在CompPowerPlantSolar中添加代表升级状态的布尔标志或等级变量。在CalculateDayNightEfficiency方法中根据这些标志调整计算逻辑。同时需要创建对应的ResearchProjectDefXML定义并在研究完成后通过游戏事件或Hediff增益效果系统将升级应用到已建造的建筑上这涉及更复杂的保存/加载逻辑。6.3 实现更精确的阴影投射与可视化原版太阳能板只检查屋顶。我们可以实现更真实的阴影系统动态阴影检测使用GenSight.LineOfSight或射线投射ShotReport.HitReportFor来检测在太阳方向随时间变化上是否有高山、城墙或其他高大建筑遮挡。可视化反馈在建筑上或地面上绘制一个动态的“阴影区域”让玩家直观地看到哪些物体会造成遮挡。这需要重写Draw或Print方法在图形层上叠加绘制。这部分代码较为复杂需要深入理解RimWorld的绘制管线。一个简单的起点是在Gizmo中不仅显示数字还用一个从绿色无遮挡到红色完全遮挡的颜色条来直观表示遮挡状态。7. 完整源码结构与关键文件回顾以下是本指南所涉及的核心文件及其作用你可以据此组织你的项目Source/CompPowerPlantSolar.cs核心逻辑组件负责动态发电计算、遮挡检测和Gizmo显示。Defs/ThingDefs/Buildings_Power.xml定义建筑在游戏内的所有静态属性如成本、尺寸、贴图路径和挂载的组件。Defs/ResearchProjectDefs/Research_Solar.xml可选定义解锁升级科技的研究项目。About/About.xmlMod的元数据决定Mod是否能在游戏中显示和加载。Languages/ChineseSimplified/Keyed/Strings.xml提供中文翻译提升Mod的友好度。Textures/Buildings/SolarGenerator.png建筑的视觉纹理。将C#项目编译后的MySolarGeneratorMod.dll放入Assemblies/把上述XML和纹理文件放到对应目录一个功能完整的太阳能发电机Mod就打包完成了。你可以将其压缩成.zip文件注意保持目录结构改后缀为.rimmod即可分享给其他玩家或上传至创意工坊。开发Mod最大的成就感莫过于看到自己写的代码在喜爱的游戏世界里真正运行起来并创造出新的玩法。从复刻一个太阳能板开始你已经掌握了定义物品、编写游戏逻辑、处理玩家交互的基本流程。接下来你可以尝试修改更多参数比如让它在极昼环境下超频发电或者与天气控制Mod联动在晴天时额外加成。记住多查阅RimWorld官方未公开但通过反编译或社区文档如RimWorld Wiki, Harmony库文档可了解的类库多利用开发模式进行实时调试遇到问题先查日志大部分难题都能在社区找到答案。