17 KiB
17 KiB
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方法来处理被动转换。
架构图
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:
<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:
<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)
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负责有过程的转换。
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类来处理被摧毁时的情况。
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
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
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
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
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
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
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)
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 version="1.0" encoding="utf-8" ?>
<Defs>
<JobDef>
<defName>Your_TransformingJob</defName>
<driverClass>YourNamespace.JobDriver_Transforming</driverClass>
<reportString>正在变形...</reportString>
</JobDef>
</Defs>