227 lines
9.9 KiB
Markdown
227 lines
9.9 KiB
Markdown
# 技术文档: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` 是如何被调用的。
|
||
```csharp
|
||
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。
|
||
```csharp
|
||
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>`。
|
||
|
||
### 阶段三:实现“植物割除” (`PlantCutting`) 功能
|
||
|
||
此阶段完全重复阶段二的模式,仅替换相应的名称 (`ARA_PlantCutting`, `ThinkNode_ConditionalAnimalShouldPlantCut`, `workType: PlantCutting`)。
|
||
|
||
### 阶段四:最终审查与打包
|
||
|
||
* **4.1**: 审查所有代码和 XML。
|
||
* **4.2**: 编译并进行游戏内测试。 |