Files
ArachnaeSwarm/Source/Documents/PawnBuildingTransformer_Design.md
2025-09-11 18:38:21 +08:00

17 KiB
Raw Permalink Blame History

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 ThingDefthingClass 需要指向这个新的 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>