# Rimatomics 电磁炮战略打击系统技术文档 ## 1. 引言 本文档旨在详细解析 Rimatomics Mod 中的电磁炮(Railgun)远程战略打击系统的实现机制。该系统允许玩家对世界地图上的任意(已加载的)地图进行精确的火力投送,是游戏中一种强大的后期战略武器。 我们将通过分析其三个核心 C# 类:`Building_Railgun`、`Verb_Railgun` 和 `WorldObject_Sabot`,来深入理解其功能、设计模式以及类之间的交互。 ## 2. 核心组件概览 整个系统由三个主要部分协同工作,各司其职: * **`Building_Railgun`**: 炮塔建筑本身,是整个系统的用户交互入口和武器平台。它负责处理玩家的瞄准指令,管理武器的属性(如射程、精度),并发起射击流程。 * **`Verb_Railgun`**: 定义了电磁炮的“射击”这一具体动作。它能够智能区分常规的本地射击和需要跨地图飞行的远程打击,并根据不同模式执行相应的逻辑。 * **`WorldObject_Sabot`**: 一个在世界地图上存在的临时对象,用于模拟炮弹在飞向目标过程中的状态。它负责处理跨地图飞行、轨迹计算,并在抵达目标后触发最终的命中效果。 ## 3. 类详解 ### 3.1 `Building_Railgun` `Building_Railgun` 是电磁炮塔的建筑类,继承自 `Building_EnergyWeapon`。它是玩家能直接看到并与之交互的实体。 #### 功能概述 此类作为系统的交互枢纽,主要负责以下功能: * **提供玩家操作界面**:通过 Gizmo(操作按钮)让玩家启动“火控任务”。 * **处理目标选择流程**:引导玩家从世界地图选择目标区域,再到目标地图内选择精确弹着点。 * **管理武器状态与属性**:存储远程目标信息,并根据已安装的升级动态计算射程、散布等属性。 * **发起射击指令**:在获取有效目标后,命令自身的武器组件(Verb)开火。 #### 源代码 ```csharp using System; using System.Collections.Generic; using System.Linq; using Multiplayer.API; using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; using Verse.Sound; namespace Rimatomics { [StaticConstructorOnStartup] public class Building_Railgun : Building_EnergyWeapon { public override bool TurretBased { get { return true; } } public override Vector3 TipOffset { get { Vector3 pos = this.DrawPos; Vector3 vecOffset = new Vector3(0f, 1f, 5f); vecOffset = vecOffset.RotatedBy(this.TurretRotation); return pos + vecOffset; } } public override bool CanFireWhileRoofed() { // For remote fire missions, cannot be roofed. For local targets, it's allowed. if (this.longTargetInt == GlobalTargetInfo.Invalid) { return true; } return !base.Position.Roofed(base.Map); } public float RangeToWorldTarget { get { return (float)Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, this.longTarget.Tile, true, int.MaxValue, false); } } public void TryChamberRound() { if (this.magazine.NullOrEmpty()) { return; } CompChangeableProjectile comp = this.gun.TryGetComp(); if (!comp.Loaded) { comp.LoadShell(this.magazine.FirstOrDefault(), 1); this.magazine.RemoveAt(0); } } public override void MuzzleFlash() { Mote flash = (Mote)ThingMaker.MakeThing(Building_Railgun.Mote_RailgunMuzzleFlash, null); flash.Scale = 5f; flash.exactRotation = this.TurretRotation; flash.exactPosition = this.TipOffset; GenSpawn.Spawn(flash, base.Position, base.Map, WipeMode.Vanish); Vector3 vecOffset = new Vector3(1f, 1f, -1f).RotatedBy(this.TurretRotation); FleckMaker.ThrowSmoke(this.DrawPos + vecOffset, base.Map, 1.5f); Vector3 vecOffset2 = new Vector3(-1f, 1f, -1f).RotatedBy(this.TurretRotation); FleckMaker.ThrowSmoke(this.DrawPos + vecOffset2, base.Map, 1.5f); } public override IEnumerable GetGizmos() { foreach (Gizmo c in base.GetGizmos()) { yield return c; } Command_Action launch = new Command_Action { defaultLabel = "critCommandFireMission".Translate(), defaultDesc = "critCommandFireMissionDesc".Translate(), icon = Building_Railgun.FireMissionTex, action = new Action(this.StartChoosingDestination) }; if (base.Spawned && base.Position.Roofed(base.Map)) { launch.Disable("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst()); } yield return launch; } private void StartChoosingDestination() { // Record all selected railguns to fire them simultaneously this.selectedRailguns = Find.Selector.SelectedObjects.OfType().ToList(); CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan); Find.WorldSelector.ClearSelection(); int tile = base.Map.Tile; Find.WorldTargeter.BeginTargeting( new Func(this.ChoseWorldTarget), // Callback when target is chosen false, Building_Railgun.FireMissionTex, true, () => GenDraw.DrawWorldRadiusRing(tile, this.WorldRange, null), // Draws the range ring null, null, null, false); } public static TargetingParameters ForFireMission() { // In multiplayer, disallow targeting mobile pawns to prevent desync issues. if (MP.IsInMultiplayer) { return new TargetingParameters { canTargetPawns = false, canTargetBuildings = false, canTargetLocations = true }; } return new TargetingParameters { canTargetPawns = true, canTargetBuildings = true, canTargetLocations = true }; } private bool ChoseWorldTarget(GlobalTargetInfo target) { if (!target.IsValid) { Messages.Message("MessageRailgunTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true); return false; } int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, target.Tile, true, int.MaxValue, false); if (distance > this.WorldRange) { Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true); return false; } MapParent mapParent = target.WorldObject as MapParent; if (mapParent != null && mapParent.HasMap) { if (mapParent.Map == base.Map) { Messages.Message("MessageRailgunCantTargetMyMap".Translate(), MessageTypeDefOf.RejectInput, true); return false; } // --- Refactored Code Block --- // This block handles the logic for fine-grained targeting within the destination map. Map targetMap = mapParent.Map; var originalMap = base.Map; // Action to execute when targeting is finished or cancelled. Action onFinished = () => { if (Current.Game.CurrentMap != originalMap) { Current.Game.CurrentMap = originalMap; } }; // Switch to the target map to allow the player to pick a precise location. Current.Game.CurrentMap = targetMap; Find.Targeter.BeginTargeting( Building_Railgun.ForFireMission(), (LocalTargetInfo localTarget) => // Lambda for when a local target is selected { // Assign the fire mission to all selected railguns. foreach (Building_Railgun railgun in this.selectedRailguns) { railgun.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID); } }, null, // Action on highlight onFinished, // Action on finish/cancel Building_Railgun.FireMissionTex, true); return true; } else { Messages.Message("MessageRailgunNeedsMap".Translate(), MessageTypeDefOf.RejectInput, true); return false; } } [SyncMethod(SyncContext.None)] public void FireMission(int tile, LocalTargetInfo targ, int map) { if (!targ.IsValid) { this.longTargetInt = GlobalTargetInfo.Invalid; return; } GlobalTargetInfo newtarget = targ.ToGlobalTargetInfo(Find.Maps.FirstOrDefault((Map x) => x.uniqueID == map)); int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, tile, true, int.MaxValue, false); if (distance > this.WorldRange) { Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true); return; } if (this.holdFire) { Messages.Message("MessageTurretWontFireBecauseHoldFire".Translate(this.def.label), this, MessageTypeDefOf.RejectInput, true); return; } if (this.longTargetInt != newtarget) { this.longTargetInt = newtarget; if (this.burstCooldownTicksLeft <= 0) { this.TryStartShootSomething(); } SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(base.Position, base.Map, false)); } } public override float PulseSize { get { float f = base.GunProps.EnergyWep.PulseSizeScaled; if (this.UG.HasUpgrade(DubDef.MEPS)) { f *= 1.15f; } if (this.UG.HasUpgrade(DubDef.ERS)) { f *= 0.85f; // Equivalent to f -= 0.15f * f } return f; } } public override int WorldRange { get { // In space (SoS2 compatibility), range is unlimited. if (base.Map != null && this.space != null && base.Map.Biome == this.space) { return 99999; } int range = base.GunProps.EnergyWep.WorldRange; if (this.UG.HasUpgrade(DubDef.TargetingChip)) { range += 10; } if (this.UG.HasUpgrade(DubDef.MEPS)) { range += 10; } return range; } } public int spread { get { int v = 6; if (this.UG.HasUpgrade(DubDef.TargetingChip)) { v -= 3; } return v; } } public static readonly Texture2D FireMissionTex = ContentFinder.Get("Rimatomics/UI/FireMission", true); public static ThingDef Mote_RailgunMuzzleFlash = ThingDef.Named("Mote_RailgunMuzzleFlash"); private List selectedRailguns; public BiomeDef space = DefDatabase.GetNamed("OuterSpaceBiome", false); } } ``` #### 关键方法和属性详解 * **`GetGizmos()`**: 为炮塔添加“火控任务” (Fire Mission) 按钮,这是远程打击流程的起点。如果炮塔被屋顶遮挡,该按钮会被禁用。 * **`StartChoosingDestination()`**: 响应按钮点击,将视角切换到世界地图,并启动一个带射程圈的目标选择器,让玩家选择目标地块。它支持多炮塔同时选择。 * **`ChoseWorldTarget(GlobalTargetInfo target)`**: 当玩家在世界地图上选择目标后的回调函数。它会进行一系列合法性验证(如射程、是否为当前地图等),如果通过,则将视角切换到目标地图,让玩家选择精确落点。 * **`FireMission(int tile, LocalTargetInfo targ, int map)`**: 这是目标选择的最后一步。它将最终的、精确的目标信息(包含地块ID和地图内坐标)存入 `longTargetInt` 变量,并立即尝试触发射击 (`TryStartShootSomething()`)。该方法支持多人游戏同步。 * **`WorldRange` (get)**: 一个计算属性,返回炮塔在世界地图上的最大射程。基础射程定义在XML中,并会受到 `TargetingChip` 和 `MEPS` 等升级的加成。 * **`spread` (get)**: 计算属性,返回炮弹的散布半径。`TargetingChip` 升级可以减小此值,提高精度。 * **`CanFireWhileRoofed()`**: 重写方法,规定在执行远程打击 (`longTargetInt` 有效) 时,炮塔不能处于屋顶之下。 ### 3.2 `Verb_Railgun` `Verb_Railgun` 继承自 `Verb_RimatomicsVerb`,它定义了电磁炮武器的“射击”这个核心动作。它不处理玩家交互,只负责执行射击逻辑。 #### 功能概述 这个类的主要职责是根据炮塔当前的目标状态(本地目标 vs 远程目标),决定执行哪种射击模式。 * **动态炮弹选择**: 根据 `CompChangeableProjectile` 组件的状态,决定是发射已装填的特殊炮弹还是默认炮弹。 * **射击模式分发**: 检查是否存在远程目标 (`longTarget`),如果存在,则执行“火控任务”流程;否则,执行标准的本地射击。 * **远程打击发起**: 在“火控任务”模式下,负责创建并初始化 `WorldObject_Sabot`,将其发射到世界地图。 * **本地打击后效**: 为本地射击附加额外的效果,如能量消耗、数据收集和屏幕震动。 #### 源代码 ```csharp using System; using RimWorld; using RimWorld.Planet; using UnityEngine; using Verse; namespace Rimatomics { [StaticConstructorOnStartup] public class Verb_Railgun : Verb_RimatomicsVerb { public override ThingDef Projectile { get { ThingWithComps equipmentSource = base.EquipmentSource; CompChangeableProjectile comp = (equipmentSource != null) ? equipmentSource.GetComp() : null; if (comp != null && comp.Loaded) { return comp.Projectile; } return this.verbProps.defaultProjectile; } } public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ) { // If a long-range target is set, we can "hit" it regardless of local range. if (base.GetWep.longTarget.IsValid) { return true; } return base.CanHitTargetFrom(root, targ); } protected bool TryCastFireMission() { Building_Railgun railgun = this.caster as Building_Railgun; // Create the flying sabot object on the world map WorldObject_Sabot sabot = (WorldObject_Sabot)WorldObjectMaker.MakeWorldObject(DefDatabase.GetNamed("Sabot", true)); sabot.railgun = railgun; sabot.Tile = railgun.Map.Tile; sabot.destinationTile = railgun.longTarget.Tile; sabot.destinationCell = railgun.longTarget.Cell; sabot.spread = railgun.spread; sabot.Projectile = this.Projectile; Find.WorldObjects.Add(sabot); // Post-launch effects and data gathering railgun.GatherData("PPCWeapon", 5f); railgun.GatherData("PPCFireMission", 10f); railgun.GatherData("PPCRailgun", 10f); railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance); railgun.MuzzleFlash(); Find.CameraDriver.shaker.SetMinShake(0.1f); // Spawn a dummy projectile that flies off-map. This is for visual effect only. Vector3 shellDirection = Vector3.forward.RotatedBy(railgun.TurretRotation); IntVec3 outcell = (railgun.DrawPos + shellDirection * 500f).ToIntVec3(); Projectile projectile2 = (Projectile)GenSpawn.Spawn(this.Projectile, railgun.Position, this.caster.Map, WipeMode.Vanish); projectile2.Launch(railgun, railgun.DrawPos, outcell, null, ProjectileHitFlags.None, false, base.EquipmentSource, null); // Handle shell consumption CompChangeableProjectile comp = base.EquipmentSource?.GetComp(); if (comp != null) { comp.Notify_ProjectileLaunched(); } railgun.DissipateCharge(railgun.PulseSize); return true; } public override bool TryCastShot() { Building_Railgun railgun = this.caster as Building_Railgun; if (!railgun.top.TargetInSights) { return false; } if (this.Projectile == null) { return false; } bool shotResult; // --- Shooting Mode Dispatch --- if (railgun.longTarget.IsValid) { // Execute long-range fire mission shotResult = this.TryCastFireMission(); } else { // Execute standard local shot shotResult = base.TryCastShot(); if (shotResult) { railgun.DissipateCharge(railgun.PulseSize); railgun.GatherData("PPCWeapon", 5f); railgun.GatherData("PPCRailgun", 10f); railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance); railgun.MuzzleFlash(); Find.CameraDriver.shaker.SetMinShake(0.1f); } } // Chamber the next round after firing railgun.TryChamberRound(); return shotResult; } } } ``` #### 关键方法和属性详解 * **`Projectile` (get)**: 这是一个动态属性,用于获取当前应该发射的炮弹类型。它会优先返回 `CompChangeableProjectile` 中已装填的炮弹,如果未装填,则返回在 XML 中定义的默认炮弹。 * **`CanHitTargetFrom(...)`**: 重写了基类方法。当炮塔被赋予了一个远程目标 (`longTarget.IsValid`) 时,该方法直接返回 `true`,绕过了所有常规的射程和视线检查,确保远程打击流程可以启动。 * **`TryCastShot()`**: 这是射击动作的入口点。它的核心逻辑是检查 `railgun.longTarget` 是否有效。 * 如果**有效**,说明是远程打击任务,它便调用 `TryCastFireMission()`。 * 如果**无效**,说明是常规的本地瞄准,它就调用基类的 `base.TryCastShot()` 来发射普通炮弹,并附加一系列开火后效。 * **`TryCastFireMission()`**: 这是发起远程打击的核心。它不直接生成命中目标的炮弹,而是: 1. 创建一个 `WorldObject_Sabot` 实例。 2. 将目标地块、精确坐标、炮弹类型、散布等关键信息从 `Building_Railgun` 传递给 `sabot` 对象。 3. 将 `sabot` 添加到世界对象管理器 (`Find.WorldObjects.Add(sabot)`),让其开始在世界地图上“飞行”。 4. 触发炮口闪光、能量消耗、屏幕震动等本地开火效果。 5. 生成一个飞向地图外的“虚拟”炮弹,仅用于视觉表现,它不会造成任何伤害。 ### 3.3 `WorldObject_Sabot` `WorldObject_Sabot` 继承自 `WorldObject`,是远程打击流程中的“飞行”阶段的执行者。它是一个临时的、在世界地图上存在的实体,模拟了炮弹从发射点到目标点的飞行过程。 #### 功能概述 此类完全独立于发射它的炮塔,其核心职责是在世界地图上完成一段旅程,并在抵达终点时触发命中效果。 * **飞行轨迹模拟**: 通过 `DrawPos` 属性和 `Tick` 方法,平滑地计算并更新自身在世界地图上的位置,实现飞行-动画效果。 * **状态持久化**: 通过 `ExposeData` 方法保存所有关键信息(如起点、终点、飞行进度、炮弹类型),确保在游戏存读档后飞行可以继续。 * **命中触发**: 在飞行到达终点后,`Arrived` 方法负责在目标地图上生成真正的 `Projectile`(炮弹),并让其从地图边缘发射,命中最终的精确弹着点。 * **自我销毁**: 完成命中逻辑后,将自身从世界对象管理器中移除。 * **Mod兼容性**: 能够识别并处理 "Save Our Ship 2" Mod 中的轨道飞船,实现地对空、空对地和空对空打击。 #### 源代码 ```csharp using System; using System.Linq; using RimWorld.Planet; using UnityEngine; using Verse; namespace Rimatomics { public class WorldObject_Sabot : WorldObject { private Vector3 Start { get { Vector3 startPos = Find.WorldGrid.GetTileCenter(this.initialTile); // SoS2 compatibility: if a ship is at the tile, use its position. if (HarmonyPatches.SoS) { WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o => (o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.initialTile); if (ship != null) { startPos = ship.DrawPos; } } return startPos; } } private Vector3 End { get { Vector3 endPos = Find.WorldGrid.GetTileCenter(this.destinationTile); // SoS2 compatibility if (HarmonyPatches.SoS) { WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o => (o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.destinationTile); if (ship != null) { endPos = ship.DrawPos; } } return endPos; } } public override Vector3 DrawPos { get { // Slerp for a smooth curve over the planet's surface return Vector3.Slerp(this.Start, this.End, this.traveledPct); } } private float TraveledPctStepPerTick { get { Vector3 start = this.Start; Vector3 end = this.End; if (start == end) return 1f; float distance = GenMath.SphericalDistance(start.normalized, end.normalized); if (distance == 0f) return 1f; // Travel speed is constant return TravelSpeed / distance; } } public override void ExposeData() { base.ExposeData(); Scribe_Values.Look(ref this.destinationTile, "destinationTile", 0, false); Scribe_Values.Look(ref this.destinationCell, "destinationCell", default(IntVec3), false); Scribe_Values.Look(ref this.arrived, "arrived", false, false); Scribe_Values.Look(ref this.initialTile, "initialTile", 0, false); Scribe_Values.Look(ref this.traveledPct, "traveledPct", 0f, false); Scribe_Defs.Look(ref this.Projectile, "Projectile"); Scribe_References.Look(ref this.railgun, "railgun"); Scribe_Values.Look(ref this.spread, "spread", 1, false); } public override void PostAdd() { base.PostAdd(); this.initialTile = base.Tile; } public override void Tick() { base.Tick(); this.traveledPct += this.TraveledPctStepPerTick; if (this.traveledPct >= 1f) { this.traveledPct = 1f; this.Arrived(); } } private void Arrived() { if (this.arrived) return; this.arrived = true; Map map = Current.Game.FindMap(this.destinationTile); if (map != null) { // Spawn the projectile at the edge of the map IntVec3 entryCell = new IntVec3(CellRect.WholeMap(map).Width / 2, 0, CellRect.WholeMap(map).maxZ); Projectile projectile = (Projectile)GenSpawn.Spawn(this.Projectile, entryCell, map, WipeMode.Vanish); // Find a random cell near the target destination within the spread radius IntVec3 finalDestination; CellFinder.TryFindRandomCellNear(this.destinationCell, map, this.spread, null, out finalDestination, -1); // Launch the projectile to the final destination projectile.Launch(this.railgun, finalDestination, finalDestination, ProjectileHitFlags.IntendedTarget, false, null); } // Remove self from the world Find.WorldObjects.Remove(this); } private const float TravelSpeed = 0.0001f; private bool arrived; public IntVec3 destinationCell = IntVec3.Invalid; public int destinationTile = -1; private int initialTile = -1; public ThingDef Projectile; public Thing railgun; public int spread = 1; private float traveledPct; } } ``` #### 关键方法和属性详解 * **`Start` / `End` (get)**: 这两个属性负责计算飞行的起点和终点坐标。它们通过检查 `HarmonyPatches.SoS` 来判断是否加载了 "Save Our Ship 2" Mod,如果加载了,则会尝试获取轨道上飞船的位置作为起点/终点,从而实现与该Mod的无缝兼容。 * **`DrawPos` (get)**: 重写属性,用于在世界地图上渲染该对象。它使用 `Vector3.Slerp`(球面线性插值)在起点和终点之间进行插值,根据 `traveledPct`(已飞行百分比)计算出当前帧应该在的位置,从而形成一条平滑的弧形飞行轨迹。 * **`Tick()`**: 游戏引擎为每个世界对象调用的更新方法。它在每一帧增加 `traveledPct` 来推进飞行进度。当 `traveledPct` 达到1时,调用 `Arrived()`。 * **`Arrived()`**: 这是飞行结束、触发命中的核心方法。它首先检查目标地块是否存在一个已加载的地图。如果存在,它会在该地图的边缘生成 `Projectile` 实体,并根据 `spread`(散布)在目标点附近随机一个最终落点,然后调用炮弹的 `Launch()` 方法完成最后的攻击。无论地图是否存在,它最终都会将自己从世界上移除。 * **`ExposeData()`**: 保证了该飞行物体的所有状态(起点、终点、进度、炮弹类型等)都能被正确地保存和加载。 ## 4. 系统工作流程 现在我们将三个核心组件联系起来,详细描述从玩家点击按钮到炮弹命中的完整工作流程。 ### 4.1 交互时序图 下面的 Mermaid 时序图直观地展示了不同对象之间的交互顺序。 ```mermaid sequenceDiagram participant Player as 玩家 participant Railgun as Building_Railgun participant Verb as Verb_Railgun participant Sabot as WorldObject_Sabot participant Projectile as 最终炮弹 Player->>Railgun: 点击 "Fire Mission" 按钮 activate Railgun Railgun->>Player: 显示世界地图和射程圈 Player->>Railgun: 1. 选择世界目标 (地块) Player->>Railgun: 2. 选择地图内目标 (坐标) Railgun->>Railgun: 调用 FireMission() 设置 longTarget Railgun->>Verb: 调用 TryStartShootSomething() deactivate Railgun activate Verb Verb->>Verb: TryCastShot() 检测到 longTarget Verb->>Verb: 调用 TryCastFireMission() Verb->>Sabot: new WorldObject_Sabot() activate Sabot Verb->>Sabot: 传递目标信息、炮弹类型等 Verb-->>Player: 触发炮口闪光和声音 deactivate Verb loop 飞行过程 (每Tick) Sabot->>Sabot: 更新 traveledPct (飞行进度) end Sabot->>Sabot: Arrived() - 到达目标 Sabot->>Projectile: GenSpawn.Spawn(炮弹) activate Projectile Sabot->>Projectile: Launch() - 飞向最终落点 Projectile-->>Player: 爆炸和伤害效果 deactivate Projectile Sabot->>Sabot: Find.WorldObjects.Remove(this) deactivate Sabot ``` ### 4.2 步骤分解 1. **启动与目标选择 (玩家 -> `Building_Railgun`)** * 玩家选中一门或多门 `Building_Railgun` 并点击其 "Fire Mission" Gizmo。 * `GetGizmos` 触发 `StartChoosingDestination` 方法,视角切换至世界地图,并显示最大射程圈。 * 玩家在世界地图上选择一个目标地块。 * `ChoseWorldTarget` 方法被回调。在通过一系列验证后,视角切换到目标地图。 * 玩家在目标地图上选择一个精确的弹着点。 2. **下达指令 (`Building_Railgun` -> `Verb_Railgun`)** * 当精确弹着点被选定后,`ChoseWorldTarget` 方法会为所有被选中的炮塔调用 `FireMission` 方法。 * `FireMission` 将包含地块和坐标的 `GlobalTargetInfo` 存入炮塔的 `longTarget` 变量中。 * `FireMission` 随即调用 `TryStartShootSomething()`,这会启动炮塔的射击冷却计时器,并最终触发其Verb组件。 3. **发射与创建飞行物 (`Verb_Railgun` -> `WorldObject_Sabot`)** * 炮塔的 `Verb_Railgun` 组件的 `TryCastShot` 方法被调用。 * `TryCastShot` 检测到 `longTarget` 是有效的,因此它不会执行常规射击,而是调用 `TryCastFireMission`。 * `TryCastFireMission` 创建一个 `WorldObject_Sabot` 的实例,并将目标信息、炮弹定义、散布等关键数据从 `Building_Railgun` 复制到这个新实例中。 * `WorldObject_Sabot` 被添加到 `Find.WorldObjects` 管理器中,正式开始其生命周期。 * 同时,`Verb_Railgun` 在本地触发开火的视觉和听觉效果。 4. **跨地图飞行 (`WorldObject_Sabot`)** * `WorldObject_Sabot` 作为一个独立的世界对象,其 `Tick` 方法被游戏引擎在每一帧调用。 * `Tick` 方法不断更新 `traveledPct` 属性,模拟飞行进度。 * 其 `DrawPos` 属性根据 `traveledPct` 在世界地图上平滑地渲染出飞行轨迹。 5. **抵达与命中 (`WorldObject_Sabot` -> `Projectile`)** * 当 `traveledPct` 达到100%时,`Arrived` 方法被调用。 * `Arrived` 检查目标地块的地图是否已加载。 * 如果地图已加载,它会在地图边缘 `GenSpawn.Spawn` 一个真正的 `Projectile`(最终炮弹)。 * 根据从 `Building_Railgun` 继承来的 `spread` 值,在玩家指定的精确落点附近随机一个最终弹着点。 * 调用 `Projectile.Launch()`,使其从地图边缘飞向并命中最终弹着点,产生爆炸和伤害。 * 最后,`WorldObject_Sabot` 调用 `Find.WorldObjects.Remove(this)` 将自己从世界上移除,完成其使命。 ## 5. 总结 Rimatomics的电磁炮系统是一个设计精良的远程打击模块。它通过将**交互(Building)**、**动作(Verb)**和**飞行(WorldObject)**三个阶段清晰地分离到不同的类中,实现了高度的内聚和低耦合。这种设计不仅使得代码逻辑清晰、易于维护,还通过 `WorldObject` 机制优雅地解决了跨地图状态同步和持久化的问题,并为兼容其他Mod(如Save Our Ship 2)留出了接口。