# 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 正在变形... ```