9.9 KiB
9.9 KiB
技术文档:RimWorld 中 specialTrainables 的工作机制解析 (V6 - 终极证据版)
1. 目标
本文档旨在深入剖析 RimWorld 中 <specialTrainables> 标签的底层工作机制,并最终得出一套可复用的“配方”,用于让动物学会在无需训练、无衰减的情况下,执行任何原版已存在的工作。本文档包含所有必要的、完整的原版代码引用作为证据,以确保其自包含性、正确性和健壮性,可供任何人在无上下文的情况下阅读并执行。
2. 核心机制深度解析
specialTrainables 的功能是一套精巧的、深度集成在 AI 思维逻辑中的 条件行为分支。
2.1. 玩家授权机制
关键发现: 游戏通过一个统一的 UI 列 (PawnColumnWorker_Trainable_Special) 来管理所有被标记为 specialTrainable 的技能。当玩家点击该列的复选框时,会调用 pawn.training.SetWantedRecursive() 来设置 Pawn_TrainingTracker 中 wantedTrainables 字段的值。
- 证据: 以下是
PawnColumnWorker_Trainable_Special.txt的完整内容,它清晰地展示了SetWantedRecursive是如何被调用的。using System.Text; using UnityEngine; using Verse; namespace RimWorld { public class PawnColumnWorker_Trainable_Special : PawnColumnWorker { public override void DoHeader(Rect rect, PawnTable table) { base.DoHeader(rect, table); MouseoverSounds.DoRegion(rect); } public override void DoCell(Rect rect, Pawn pawn, PawnTable table) { if (pawn.training != null && !pawn.RaceProps.specialTrainables.NullOrEmpty()) { int num = (int)((rect.width - 24f) / 2f); int num2 = Mathf.Max(3, 0); Rect rect2 = new Rect(rect.x + (float)num, rect.y + (float)num2, 24f, 24f); DoSpecialTrainableCheckbox(rect2, pawn, doTooltip: true); } } private void DoSpecialTrainableCheckbox(Rect rect, Pawn pawn, bool doTooltip) { GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var _); bool flag = checkOn; Texture2D texChecked = (learned ? TrainingCardUtility.LearnedTrainingTex : null); Texture2D texUnchecked = (learned ? TrainingCardUtility.LearnedNotTrainingTex : null); Widgets.Checkbox(rect.position, ref checkOn, rect.width, !canTrain, paintable: true, texChecked, texUnchecked); if (checkOn != flag) { PlayerKnowledgeDatabase.KnowledgeDemonstrated(ConceptDefOf.AnimalTraining, KnowledgeAmount.Total); foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables) { pawn.training.SetWantedRecursive(specialTrainable, checkOn); } } if (doTooltip) { DoSpecialTrainableTooltip(rect, pawn); } } private void DoSpecialTrainableTooltip(Rect rect, Pawn pawn) { if (!Mouse.IsOver(rect)) { return; } TooltipHandler.TipRegion(rect, delegate { StringBuilder stringBuilder = new StringBuilder(); foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables) { bool visible; AcceptanceReport acceptanceReport = pawn.training.CanAssignToTrain(specialTrainable, out visible); stringBuilder.AppendLineIfNotEmpty(); stringBuilder.AppendLine(specialTrainable.LabelCap + "\n\n" + specialTrainable.description); if (!acceptanceReport.Accepted) { stringBuilder.AppendLine().AppendLine(acceptanceReport.Reason); } else if (!specialTrainable.prerequisites.NullOrEmpty()) { stringBuilder.AppendLine(); foreach (TrainableDef prerequisite in specialTrainable.prerequisites) { if (!pawn.training.HasLearned(prerequisite)) { stringBuilder.AppendLine("TrainingNeedsPrerequisite".Translate(prerequisite.LabelCap)); } } } } return stringBuilder.ToString(); }, (int)(rect.y * 511f + rect.x)); } public override int GetMinWidth(PawnTable table) { return Mathf.Max(base.GetMinWidth(table), 24); } public override int GetMaxWidth(PawnTable table) { return Mathf.Min(base.GetMaxWidth(table), GetMinWidth(table)); } public override int GetMinCellHeight(Pawn pawn) { return Mathf.Max(base.GetMinCellHeight(pawn), 24); } public override int Compare(Pawn a, Pawn b) { return GetValueToCompare(a).CompareTo(GetValueToCompare(b)); } private int GetValueToCompare(Pawn pawn) { if (pawn.training == null || pawn.RaceProps.specialTrainables.NullOrEmpty()) { return int.MinValue; } GetStatus(pawn, out var learned, out var checkOn, out var canTrain, out var visible); if (learned) { return 4; } if (!visible) { return 0; } if (!canTrain) { return 1; } if (!checkOn) { return 2; } return 3; } private static void GetStatus(Pawn pawn, out bool learned, out bool checkOn, out bool canTrain, out bool visible) { learned = true; checkOn = true; canTrain = true; visible = false; foreach (TrainableDef specialTrainable in pawn.RaceProps.specialTrainables) { if (!pawn.training.HasLearned(specialTrainable)) { learned = false; } if (!pawn.training.GetWanted(specialTrainable)) { checkOn = false; } if (!pawn.training.CanAssignToTrain(specialTrainable, out var visible2)) { canTrain = false; } if (visible2) { visible = true; } } } } }
2.2. AI 决策机制
- 关键发现: 我们可以为每个技能创建一个继承自
ThinkNode_Conditional的决策节点,在Satisfied方法中通过检查pawn.training.GetWanted(def)来判断玩家是否授权。
3. 最终实施蓝图
阶段一:核心机制实现 (通用)
- 1.1: 实现瞬间训练 (
CompInstantTrain.cs): 创建一个ThingComp,在PostSpawnSetup中调用pawn.training.Train(def, null, true)。 - 1.2: 引入 Harmony: 在项目中添加
0Harmony.dll引用并创建MainHarmony.cs初始化补丁。 - 1.3: 阻止训练衰减 (
Patch_TrainingTracker_TickRare.cs): 创建一个对Pawn_TrainingTracker.TrainingTrackerTickRare的前缀补丁,对特殊动物返回false。
阶段二:实现“种植” (Growing) 功能
- 2.1: (XML) 创建
TrainableDef: 创建Defs/TrainableDefs/ARA_Sowing.xml,定义ARA_Sowing,并设<specialTrainable>true</specialTrainable>。 - 2.2: (C#) 创建 AI 决策节点: 创建
Source/ArachnaeSwarm/ThinkNode_ConditionalAnimalShouldSow.cs,并使用静态缓存 Def。using Verse; using Verse.AI; namespace ArachnaeSwarm { [DefOf] public static class ARA_TrainableDefOf { public static TrainableDef ARA_Sowing; public static TrainableDef ARA_PlantCutting; static ARA_TrainableDefOf() { DefOfHelper.EnsureInitializedInCtor(typeof(ARA_TrainableDefOf)); } } public class ThinkNode_ConditionalAnimalShouldSow : ThinkNode_Conditional { protected override bool Satisfied(Pawn pawn) { if (pawn.training == null) return false; return pawn.training.HasLearned(ARA_TrainableDefOf.ARA_Sowing) && pawn.training.GetWanted(ARA_TrainableDefOf.ARA_Sowing); } } } - 2.3: (XML) 关联组件:
- PawnKindDef: 在
comps列表中添加CompProperties_InstantTrain并配置trainables。 - ThingDef: 在
specialTrainables列表中添加<li>ARA_Sowing</li>。 - ThinkTreeDef: 添加
<li Class="ArachnaeSwarm.ThinkNode_ConditionalAnimalShouldSow">节点,其子节点为<li Class="RimWorld.JobGiver_Work"><workType>Growing</workType></li>。
- PawnKindDef: 在
阶段三:实现“植物割除” (PlantCutting) 功能
此阶段完全重复阶段二的模式,仅替换相应的名称 (ARA_PlantCutting, ThinkNode_ConditionalAnimalShouldPlantCut, workType: PlantCutting)。
阶段四:最终审查与打包
- 4.1: 审查所有代码和 XML。
- 4.2: 编译并进行游戏内测试。