# Pawn 与 Building 双向转换系统设计文档
本文档旨在详细阐述一个通用的、可配置的系统,用于在《边缘世界》中实现角色(Pawn)与建筑(Building)之间的双向转换。
## 1. 核心目标
- **通用性**: 任何Pawn或Building都可以通过简单的XML配置获得变形能力。
- **健壮性**: 确保在各种情况下(主动触发、被动摧毁、存档读档)Pawn的状态都能被正确保存和恢复。
- **可扩展性**: 方便开发者添加自定义的转换条件、资源消耗和效果。
- **状态保持**: 明确Pawn的关键状态(如健康、技能、装备、意识形态等)在转换过程中通过保存Pawn实例本身而得以完整保留。
## 2. 系统架构
系统由以下几个核心部分组成:
- **`CompProperties_Morphable`**: 在XML中定义的配置组件,用于指定变形的目标、效果和耗时。
- **`CompMorphable`**: 附加在物体上的核心逻辑组件,管理变形状态和触发器。
- **`CompThingContainer`**: 用于在建筑形态下实际“存储”Pawn对象的容器。
- **`JobDriver_Transforming`**: 一个自定义的`JobDriver`,用于处理从Pawn到Building的、有持续时间的转换过程。
- **自定义Building类**: 需要一个继承自`Building`的子类,以重写`Destroy`方法来处理被动转换。
### 架构图
```mermaid
graph TD
subgraph Pawn
A[Pawn_Character] -- Has --> B[CompMorphable];
end
subgraph Building
D[Building_Morphed] -- Has --> E[CompMorphable];
D -- Also has --> C[CompThingContainer];
C -- Contains --> F[Stored Pawn Instance];
end
subgraph User_Interface
G[Gizmo Button 'Transform'] -- Triggers --> H{StartTransform};
end
A -- Transform --> D;
D -- Transform Back --> A;
B -- Initiates Job --> J[JobDriver_Transforming];
E -- Calls method --> K[TransformBackLogic];
subgraph XML_Configuration
L[ThingDef: Pawn_Character] -- Defines --> B;
M[ThingDef: Building_Morphed] -- Defines --> E & C;
end
```
---
## 3. 实现细节与代码示例
### 3.1 XML 配置 (`CompProperties_Morphable`)
为你的Pawn和Building `ThingDef` 添加以下组件。
**Pawn Def:**
```xml
Your_BuildingDef_Name
Your_TransformSound
Your_TransformEffect
120
```
**Building Def:**
```xml
CompThingContainer
Your_PawnDef_Name
Your_TransformBackSound
Your_TransformBackEffect
```
### 3.2 核心逻辑组件 (`CompMorphable`)
```csharp
using RimWorld;
using Verse;
public class CompMorphable : ThingComp
{
private Pawn storedPawn;
public CompProperties_Morphable Props => (CompProperties_Morphable)props;
public bool IsMorphed => parent.def == Props.buildingDef;
public Pawn StoredPawn => storedPawn;
public void SetStoredPawn(Pawn pawn)
{
this.storedPawn = pawn;
}
public override void PostExposeData()
{
base.PostExposeData();
// 确保Pawn引用在存档/读档时被正确保存
Scribe_References.Look(ref storedPawn, "storedPawn");
}
public override IEnumerable CompGetGizmosExtra()
{
// 为Pawn添加“变形”按钮
if (parent is Pawn pawn)
{
yield return new Command_Action
{
defaultLabel = "变形",
action = () => {
// 创建并分配变形任务
var job = new Job(JobDefOf.Your_TransformingJob, parent);
pawn.jobs.TryTakeOrderedJob(job);
}
};
}
// 为Building添加“恢复”按钮
else if (parent is Building)
{
yield return new Command_Action
{
defaultLabel = "恢复人形",
action = () => { TransformBackToPawn(); }
};
}
}
private void TransformBackToPawn()
{
// ... (此处实现Building -> Pawn的逻辑) ...
// 检查空间、移除建筑、重新生成Pawn
}
}
```
### 3.3 Pawn -> Building 转换 (`JobDriver_Transforming`)
这个`JobDriver`负责有过程的转换。
```csharp
public class JobDriver_Transforming : JobDriver
{
protected override IEnumerable MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
// 1. 检查空间
yield return Toils_General.Do(() => {
// ... 检查逻辑 ...
// 如果失败: EndJobWith(JobCondition.Incompletable);
});
// 2. 等待并播放特效
var props = pawn.GetComp().Props;
yield return Toils_General.Wait(props.transformTicks)
.WithProgressBarToilDelay(TargetIndex.A);
// 3. 执行核心转换
Toil transform = Toils_General.Do(() => {
// 移除Pawn
pawn.DeSpawn(DestroyMode.Vanish);
// 生成Building
Building building = (Building)GenSpawn.Spawn(props.buildingDef, pawn.Position, pawn.Map);
building.SetFaction(pawn.Faction);
// 存储Pawn
building.GetComp().GetDirectlyHeldThings().TryAdd(pawn);
building.GetComp().SetStoredPawn(pawn);
});
yield return transform;
}
}
```
### 3.4 Building -> Pawn 转换 (被动触发)
需要一个自定义的Building类来处理被摧毁时的情况。
```csharp
public class Building_Morphable : Building
{
public override void Destroy(DestroyMode mode)
{
var comp = this.GetComp();
if (comp != null && comp.StoredPawn != null)
{
Pawn pawn = comp.StoredPawn;
// 在建筑消失前,先把Pawn生成出来
GenSpawn.Spawn(pawn, this.Position, this.Map);
// 可选:对Pawn施加伤害
if (mode == DestroyMode.KillFinalize)
{
pawn.TakeDamage(new DamageInfo(DamageDefOf.Bomb, 20));
}
}
base.Destroy(mode);
}
}
```
**注意**: 你的Building `ThingDef` 的 `thingClass` 需要指向这个新的 `Building_Morphable` 类。
---
## 4. 流程图
### Pawn -> Building
```mermaid
sequenceDiagram
participant Player
participant Pawn
participant Gizmo
participant JobDriver_Transforming
Player->>Gizmo: 点击“变形”按钮
Gizmo->>Pawn: 分配 "Transforming" 任务
Pawn->>JobDriver_Transforming: 开始执行任务
JobDriver_Transforming->>JobDriver_Transforming: Toil 1: 检查空间
JobDriver_Transforming->>JobDriver_Transforming: Toil 2: 等待 & 播放特效
JobDriver_Transforming->>JobDriver_Transforming: Toil 3: DeSpawn Pawn, Spawn Building, Store Pawn
JobDriver_Transforming->>Pawn: 任务结束
```
### Building -> Pawn
```mermaid
graph TD
subgraph Active_Trigger [主动触发]
A[Player Clicks Gizmo] --> B{TransformBackToPawn};
B --> C[Check Spawn Area];
C -- OK --> E[DeSpawn Building];
E --> F[Spawn Pawn at Position];
end
subgraph Passive_Trigger [被动触发]
H[Building HP <= 0] --> I{Override 'Destroy' method};
I --> J[Get Stored Pawn];
J --> K[Spawn Pawn at Position];
K --> L[Apply Damage to Pawn];
L --> M[Call base.Destroy()];
end
```
这份文档提供了从设计理念到具体实现的全套方案,您可以根据此指南开始编码工作。
---
## 5. 完整C#代码实现
以下是实现此系统所需的全部C#脚本。建议将它们放置在项目的一个新子目录中(例如 `Source/Morphable/`)。
### 5.1 `CompProperties_Morphable.cs`
```csharp
using RimWorld;
using Verse;
namespace YourNamespace
{
public class CompProperties_Morphable : CompProperties
{
// 变形为建筑的目标ThingDef
public ThingDef buildingDef;
// 从建筑恢复时,指定要生成的PawnDef(可选,为空则恢复原Pawn)
public ThingDef pawnDef;
// 变形时的音效
public SoundDef transformSound;
// 变形时的视觉效果
public EffecterDef transformEffect;
// 变形过程所需的tick数
public int transformTicks = 60;
public CompProperties_Morphable()
{
compClass = typeof(CompMorphable);
}
}
}
```
### 5.2 `CompMorphable.cs`
```csharp
using RimWorld;
using System.Collections.Generic;
using Verse;
using Verse.AI;
namespace YourNamespace
{
public class CompMorphable : ThingComp
{
private Pawn storedPawn;
public CompProperties_Morphable Props => (CompProperties_Morphable)props;
public bool IsMorphed => parent.def == Props.buildingDef;
public Pawn StoredPawn => storedPawn;
public void SetStoredPawn(Pawn pawn)
{
this.storedPawn = pawn;
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_References.Look(ref storedPawn, "storedPawn", false);
}
public override IEnumerable CompGetGizmosExtra()
{
if (parent is Pawn pawn && pawn.Faction == Faction.OfPlayer)
{
yield return new Command_Action
{
defaultLabel = "变形",
defaultDesc = "将此单位转换为建筑形态。",
icon = TexCommand.Attack, // TODO: Replace with a proper icon
action = () => {
// 添加配置验证
if (Props.buildingDef == null)
{
Log.Error($"CompMorphable on {parent.def.defName} has no buildingDef defined.");
return;
}
var job = JobMaker.MakeJob(JobDefOf.Your_TransformingJob, parent);
pawn.jobs.TryTakeOrderedJob(job);
}
};
}
else if (parent is Building building && building.Faction == Faction.OfPlayer && storedPawn != null)
{
yield return new Command_Action
{
defaultLabel = "恢复人形",
defaultDesc = "将此建筑恢复为人形。",
icon = TexCommand.ReleaseAnimals, // TODO: Replace with a proper icon
action = () => { TransformBackToPawn(); }
};
}
}
private void TransformBackToPawn()
{
Building building = (Building)this.parent;
Map map = building.Map;
// 播放特效
if (Props.transformEffect != null)
{
Effecter eff = Props.transformEffect.Spawn();
eff.Trigger(building, building);
}
if (Props.transformSound != null)
{
Props.transformSound.PlayOneShot(new TargetInfo(building.Position, map));
}
// 移除建筑
building.DeSpawn(DestroyMode.Vanish);
// 重新生成Pawn
GenSpawn.Spawn(storedPawn, building.Position, map, WipeMode.Vanish);
// 选中Pawn
if (Find.Selector.IsSelected(building))
{
Find.Selector.Select(storedPawn);
}
}
}
}
```
### 5.3 `JobDriver_Transforming.cs`
```csharp
using RimWorld;
using System.Collections.Generic;
using Verse;
using Verse.AI;
namespace YourNamespace
{
public class JobDriver_Transforming : JobDriver
{
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(pawn.Position, job);
}
protected override IEnumerable MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
var comp = pawn.GetComp();
if (comp == null) yield break;
// 1. 检查空间是否足够
Toil checkSpace = Toils_General.Do(() => {
foreach (var cell in GenRadial.RadialCellsAround(pawn.Position, comp.Props.buildingDef.Size.x / 2f, true))
{
if (!cell.InBounds(Map) || !cell.Walkable(Map) || cell.GetEdifice(Map) != null)
{
pawn.jobs.EndCurrentJob(JobCondition.Incompletable, true);
if (pawn.Faction == Faction.OfPlayer)
{
Messages.Message("PawnTransformer_SpaceBlocked".Translate(pawn.Named("PAWN")), pawn, MessageTypeDefOf.RejectInput, false);
}
return;
}
}
});
yield return checkSpace;
// 2. 等待并播放特效
Toil waitingToil = Toils_General.Wait(comp.Props.transformTicks, TargetIndex.None)
.WithProgressBarToilDelay(TargetIndex.A);
waitingToil.tickAction = () =>
{
if (comp.Props.transformEffect != null && pawn.IsHashIntervalTick(5))
{
Effecter eff = comp.Props.transformEffect.Spawn();
eff.Trigger(pawn, pawn);
}
};
if (comp.Props.transformSound != null)
{
waitingToil.initAction = () => { comp.Props.transformSound.PlayOneShot(new TargetInfo(pawn.Position, pawn.Map)); };
}
yield return waitingToil;
// 3. 执行核心转换
Toil transform = Toils_General.Do(() => {
IntVec3 position = pawn.Position;
Map map = pawn.Map;
// 移除Pawn
pawn.DeSpawn(DestroyMode.Vanish);
// 生成Building
Building building = (Building)GenSpawn.Spawn(comp.Props.buildingDef, position, map, WipeMode.Vanish);
building.SetFaction(pawn.Faction);
// 继承Pawn的名称
if (pawn.Name != null)
{
building.TryGetComp().TryAssignPawn(pawn);
}
// 存储Pawn
var container = building.GetComp();
if (container != null)
{
container.GetDirectlyHeldThings().TryAdd(pawn);
}
var newMorphComp = building.GetComp();
if (newMorphComp != null)
{
newMorphComp.SetStoredPawn(pawn);
}
});
yield return transform;
}
}
}
```
### 5.4 `Building_Morphable.cs`
```csharp
using RimWorld;
using Verse;
namespace YourNamespace
{
public class Building_Morphable : Building
{
public override void Destroy(DestroyMode mode)
{
var comp = this.GetComp();
if (comp != null && comp.StoredPawn != null)
{
Pawn pawn = comp.StoredPawn;
Map map = this.Map;
IntVec3 position = this.Position;
// 在建筑消失前,先把Pawn生成出来
GenSpawn.Spawn(pawn, position, map, WipeMode.Vanish);
// 如果是被摧毁,对Pawn施加伤害
if (mode == DestroyMode.KillFinalize)
{
Messages.Message($"{pawn.LabelShort} 从被摧毁的建筑中弹出!", pawn, MessageTypeDefOf.NegativeEvent);
pawn.TakeDamage(new DamageInfo(DamageDefOf.Bomb, 20, 1, -1, this));
}
}
base.Destroy(mode);
}
}
}
```
### 5.5 `JobDefOf.cs` (用于定义JobDef)
```csharp
using RimWorld;
using Verse;
namespace YourNamespace
{
[DefOf]
public static class JobDefOf
{
public static JobDef Your_TransformingJob;
static JobDefOf()
{
DefOfHelper.EnsureInitializedInCtor(typeof(JobDefOf));
}
}
}
```
**最后,您还需要在 `Defs/JobDefs/` 目录下创建一个XML文件来定义 `Your_TransformingJob`:**
```xml
Your_TransformingJob
YourNamespace.JobDriver_Transforming
正在变形...
```