558 lines
17 KiB
Markdown
558 lines
17 KiB
Markdown
# 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
|
||
<ThingDef ParentName="BasePawn">
|
||
<!-- ... other pawn properties ... -->
|
||
<comps>
|
||
<li Class="YourNamespace.CompProperties_Morphable">
|
||
<buildingDef>Your_BuildingDef_Name</buildingDef>
|
||
<transformSound>Your_TransformSound</transformSound>
|
||
<transformEffect>Your_TransformEffect</transformEffect>
|
||
<transformTicks>120</transformTicks> <!-- 2 seconds -->
|
||
</li>
|
||
</comps>
|
||
</ThingDef>
|
||
```
|
||
|
||
**Building Def:**
|
||
```xml
|
||
<ThingDef ParentName="BuildingBase">
|
||
<!-- ... other building properties ... -->
|
||
<comps>
|
||
<!-- 容器组件,必须有 -->
|
||
<li Class="CompProperties_ThingContainer">
|
||
<compClass>CompThingContainer</compClass>
|
||
</li>
|
||
<!-- 变形逻辑组件 -->
|
||
<li Class="YourNamespace.CompProperties_Morphable">
|
||
<!-- pawnDef是可选的,如果为空则恢复被存储的pawn -->
|
||
<pawnDef>Your_PawnDef_Name</pawnDef>
|
||
<transformSound>Your_TransformBackSound</transformSound>
|
||
<transformEffect>Your_TransformBackEffect</transformEffect>
|
||
</li>
|
||
</comps>
|
||
</ThingDef>
|
||
```
|
||
|
||
### 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<Gizmo> 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<Toil> MakeNewToils()
|
||
{
|
||
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
|
||
|
||
// 1. 检查空间
|
||
yield return Toils_General.Do(() => {
|
||
// ... 检查逻辑 ...
|
||
// 如果失败: EndJobWith(JobCondition.Incompletable);
|
||
});
|
||
|
||
// 2. 等待并播放特效
|
||
var props = pawn.GetComp<CompMorphable>().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<CompThingContainer>().GetDirectlyHeldThings().TryAdd(pawn);
|
||
building.GetComp<CompMorphable>().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<CompMorphable>();
|
||
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<Gizmo> 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<Toil> MakeNewToils()
|
||
{
|
||
this.FailOnDespawnedNullOrForbidden(TargetIndex.A);
|
||
var comp = pawn.GetComp<CompMorphable>();
|
||
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<CompAssignableToPawn>().TryAssignPawn(pawn);
|
||
}
|
||
|
||
// 存储Pawn
|
||
var container = building.GetComp<CompThingContainer>();
|
||
if (container != null)
|
||
{
|
||
container.GetDirectlyHeldThings().TryAdd(pawn);
|
||
}
|
||
|
||
var newMorphComp = building.GetComp<CompMorphable>();
|
||
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<CompMorphable>();
|
||
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
|
||
<?xml version="1.0" encoding="utf-8" ?>
|
||
<Defs>
|
||
<JobDef>
|
||
<defName>Your_TransformingJob</defName>
|
||
<driverClass>YourNamespace.JobDriver_Transforming</driverClass>
|
||
<reportString>正在变形...</reportString>
|
||
</JobDef>
|
||
</Defs>
|
||
``` |