Files
ArachnaeSwarm/Source/Documents/Railgun_System_Documentation.md
2025-10-04 15:05:38 +08:00

28 KiB
Raw Blame History

Rimatomics 电磁炮战略打击系统技术文档

1. 引言

本文档旨在详细解析 Rimatomics Mod 中的电磁炮Railgun远程战略打击系统的实现机制。该系统允许玩家对世界地图上的任意已加载的地图进行精确的火力投送是游戏中一种强大的后期战略武器。

我们将通过分析其三个核心 C# 类:Building_RailgunVerb_RailgunWorldObject_Sabot,来深入理解其功能、设计模式以及类之间的交互。

2. 核心组件概览

整个系统由三个主要部分协同工作,各司其职:

  • Building_Railgun: 炮塔建筑本身,是整个系统的用户交互入口和武器平台。它负责处理玩家的瞄准指令,管理武器的属性(如射程、精度),并发起射击流程。
  • Verb_Railgun: 定义了电磁炮的“射击”这一具体动作。它能够智能区分常规的本地射击和需要跨地图飞行的远程打击,并根据不同模式执行相应的逻辑。
  • WorldObject_Sabot: 一个在世界地图上存在的临时对象,用于模拟炮弹在飞向目标过程中的状态。它负责处理跨地图飞行、轨迹计算,并在抵达目标后触发最终的命中效果。

3. 类详解

3.1 Building_Railgun

Building_Railgun 是电磁炮塔的建筑类,继承自 Building_EnergyWeapon。它是玩家能直接看到并与之交互的实体。

功能概述

此类作为系统的交互枢纽,主要负责以下功能:

  • 提供玩家操作界面:通过 Gizmo操作按钮让玩家启动“火控任务”。
  • 处理目标选择流程:引导玩家从世界地图选择目标区域,再到目标地图内选择精确弹着点。
  • 管理武器状态与属性:存储远程目标信息,并根据已安装的升级动态计算射程、散布等属性。
  • 发起射击指令在获取有效目标后命令自身的武器组件Verb开火。

源代码

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<ThingDef>())
			{
				return;
			}

			CompChangeableProjectile comp = this.gun.TryGetComp<CompChangeableProjectile>();
			if (!comp.Loaded)
			{
				comp.LoadShell(this.magazine.FirstOrDefault<ThingDef>(), 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<Gizmo> 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<Building_Railgun>().ToList<Building_Railgun>();
			
			CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
			Find.WorldSelector.ClearSelection();
			
			int tile = base.Map.Tile;
			Find.WorldTargeter.BeginTargeting(
				new Func<GlobalTargetInfo, bool>(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<Texture2D>.Get("Rimatomics/UI/FireMission", true);
		public static ThingDef Mote_RailgunMuzzleFlash = ThingDef.Named("Mote_RailgunMuzzleFlash");
		private List<Building_Railgun> selectedRailguns;
		public BiomeDef space = DefDatabase<BiomeDef>.GetNamed("OuterSpaceBiome", false);
	}
}

关键方法和属性详解

  • GetGizmos(): 为炮塔添加“火控任务” (Fire Mission) 按钮,这是远程打击流程的起点。如果炮塔被屋顶遮挡,该按钮会被禁用。
  • StartChoosingDestination(): 响应按钮点击,将视角切换到世界地图,并启动一个带射程圈的目标选择器,让玩家选择目标地块。它支持多炮塔同时选择。
  • ChoseWorldTarget(GlobalTargetInfo target): 当玩家在世界地图上选择目标后的回调函数。它会进行一系列合法性验证(如射程、是否为当前地图等),如果通过,则将视角切换到目标地图,让玩家选择精确落点。
  • FireMission(int tile, LocalTargetInfo targ, int map): 这是目标选择的最后一步。它将最终的、精确的目标信息包含地块ID和地图内坐标存入 longTargetInt 变量,并立即尝试触发射击 (TryStartShootSomething())。该方法支持多人游戏同步。
  • WorldRange (get): 一个计算属性返回炮塔在世界地图上的最大射程。基础射程定义在XML中并会受到 TargetingChipMEPS 等升级的加成。
  • spread (get): 计算属性,返回炮弹的散布半径。TargetingChip 升级可以减小此值,提高精度。
  • CanFireWhileRoofed(): 重写方法,规定在执行远程打击 (longTargetInt 有效) 时,炮塔不能处于屋顶之下。

3.2 Verb_Railgun

Verb_Railgun 继承自 Verb_RimatomicsVerb,它定义了电磁炮武器的“射击”这个核心动作。它不处理玩家交互,只负责执行射击逻辑。

功能概述

这个类的主要职责是根据炮塔当前的目标状态(本地目标 vs 远程目标),决定执行哪种射击模式。

  • 动态炮弹选择: 根据 CompChangeableProjectile 组件的状态,决定是发射已装填的特殊炮弹还是默认炮弹。
  • 射击模式分发: 检查是否存在远程目标 (longTarget),如果存在,则执行“火控任务”流程;否则,执行标准的本地射击。
  • 远程打击发起: 在“火控任务”模式下,负责创建并初始化 WorldObject_Sabot,将其发射到世界地图。
  • 本地打击后效: 为本地射击附加额外的效果,如能量消耗、数据收集和屏幕震动。

源代码

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<CompChangeableProjectile>() : 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<WorldObjectDef>.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<CompChangeableProjectile>();
			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 中的轨道飞船,实现地对空、空对地和空对空打击。

源代码

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<int>(ref this.destinationTile, "destinationTile", 0, false);
			Scribe_Values.Look<IntVec3>(ref this.destinationCell, "destinationCell", default(IntVec3), false);
			Scribe_Values.Look<bool>(ref this.arrived, "arrived", false, false);
			Scribe_Values.Look<int>(ref this.initialTile, "initialTile", 0, false);
			Scribe_Values.Look<float>(ref this.traveledPct, "traveledPct", 0f, false);
			Scribe_Defs.Look<ThingDef>(ref this.Projectile, "Projectile");
			Scribe_References.Look<Thing>(ref this.railgun, "railgun");
			Scribe_Values.Look<int>(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 时序图直观地展示了不同对象之间的交互顺序。

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留出了接口。