查看源码下载文档
CHAPTER / 06

战斗:一次开火如何变成扣血、死亡与连锁反应

《红色警戒》的战斗系统可以理解成一条分工明确、但带有大量兵种例外的处理链:单位决定是否开火,武器给出参数,弹体负责飞行和引爆,弹头决定对装甲的效果,受击对象负责生命值及死亡后果。看懂这条链,比背诵每种坦克的攻击力更接近理解游戏制作原理。

本文依据提交 0dc09bb1d6188d1ec79ab4765b22a48b75ffee32。数值由类型默认值、INI 规则、阵营修正和编译分支共同决定;示例的假设数值不代表某个发行版的标准兵种数据。

1. 武器、弹体、弹头为什么要拆成三种对象

层次 回答的问题 主要数据
WeaponTypeClass 发射什么、多久一次、射多远? Damage、ROF、Range、Speed、Burst、Projectile、Warhead
BulletTypeClass / BulletClass 如何飞行、何时爆炸? 是否抛物线、追踪转速、对空/对地/反潜、引信、坐标、责任人
WarheadTypeClass 命中后对什么有效? Verses 装甲倍率、Spread、墙体/矿石破坏、爆炸及步兵死亡类型

WeaponTypeClass::Read_INI 读取武器参数,再通过类型引用连到弹体和弹头;WarheadTypeClass::Read_INIVerses 的百分比读入按装甲枚举索引的倍率表。同一个炮弹飞行方式可以配不同弹头,同一弹头也能服务多种武器。这是组合式配置,不要求为每种枪炮复制一套伤害函数。WEAPON.CPP:204–238WARHEAD.CPP:168–190

因此,给坦克增加对空能力不能只增大伤害或射程;允许攻击空中目标的标志在弹体类型上。反过来,武器“看起来是导弹”也不等于它具备追踪能力,真正决定转向的是弹体的 ROT

2. 选择武器与判断能否开火是两件事

What_Weapon_Should_I_Use 为主副武器评分:以对目标装甲的弹头倍率为基础,处于射程内加倍,若 Can_Fire 返回完全不允许或目标非法则清零,副武器严格高于主武器才选副武器。它并没有完整计算“伤害÷射速”的 DPS。由此可知,设计双武器单位时,不能假定 AI 总会选择理论输出最高的一把。TECHNO.CPP:338–379

通用 TechnoClass::Can_Fire 依次检查目标合法性、完全隐形目标、自己是否正在下落、武器是否存在、弹体对空/对地能力、装填计时 Arm、射程、弹药和自己的隐形状态。它返回 FIRE_RANGEFIRE_REARMFIRE_AMMOFIRE_CLOAKED 等原因,而不是单个布尔值。调用方据此可以走近、等待、补弹或显形。TECHNO.CPP:2663–2759

射程也并非简单比较两个格子中心。对象指针重载从 Fire_Coord 测到目标中心,目标编码重载则使用 As_Coord 得到的 Target_Coord;目标是建筑时,根据建筑宽高额外增加容差。这给大建筑提供近似的边缘命中距离,避免玩家已经贴近建筑边缘,却因建筑中心太远而不能开火。TECHNO.CPP:1278–1317TECHNO.CPP:1242–1257TARGET.CPP:487–511

派生类再加限制:船会检查鱼雷路线中的陆地和友军、反潜能力、炮塔方向;固定翼飞机检查机头朝向。车辆 Firing_AI 收到 FIRE_FACING 会转炮塔,收到 FIRE_CLOAKED 会显形。一个“攻击”指令于是被拆成一系列能在后续帧继续的动作。UNIT.CPP:643–686VESSEL.CPP:1023–1139

3. 开火不仅创建一颗子弹

TechnoClass::Fire_At 确定目标坐标、枪口坐标和初始方向,把正伤害乘以对象及 House 的火力倍率;负伤害不经这一段正伤害加成。对特定对空加速武器,还会乘 Rule.TurboBoost。随后建立 BulletClass,保存目标、发射者 Payback、伤害、弹头和速度。TECHNO.CPP:3023–3099

创建成功后还要处理炮口闪光、后坐、音效、弹药递减、重新装填和暴露开火位置。移动中的地面移动对象发射时会将子弹标记为 IsInaccurate。这些内容集中在一个入口,保证不同兵种共享开火的公共副作用。TECHNO.CPP:3101–3215

弹药还有一个常见哨兵值:类型默认 MaxAmmo 为 -1;通用开火判定只把零当作弹尽,开火后也只递减正数,所以负值可以表示不耗尽的弹药。有限弹药与装填冷却是两种独立限制。TECHNO.CPP:5995TECHNO.CPP:3186–3188

“双发”采用交替状态:Is_Two_Shooter 在主副武器相同或主武器 Burst > 1 时返回真;IsSecondShot 在发射后翻转。Rearm_Delay 在相应长间隔分支返回 ROF × House->ROFBias,否则短间隔是 3;建筑还有弹药大于 1 时返回 1 的分支。因此不能把 Burst=5 在这段代码中直译成通用五连发系统;这里确认的是二次交替行为,具体兵种还可能覆盖调用方式。TECHNO.CPP:2857–2869TECHNO.CPP:6188–6193

泰斯拉闪电则展示了表现与伤害的区别。普通开火入口仍创建弹体,再调用 Electric_Zap 绘制多条曲折闪电;闪电函数主体在画面上拼形状,没有沿着屏幕折线逐点结算受伤。视觉上经过某单位,不自动等于电流碰撞了它。TECHNO.CPP:2897–2998

4. 飞行是每帧状态更新,命中不是现代刚体碰撞

BulletClass::Unlimbo 初始化飞行。散布条件不只来自“移动射击”:类型本身不精确、某些穿甲弹攻击步兵/地面格、带燃料弹体等也能触发。散布距离随距离计算,再受规则上限约束。抛物线弹体会同时扰动方向和目标点,其他分支主要沿方向移动目标点。BULLET.CPP:677–729

不可见且为光速的弹体直接把坐标移到目标位置;普通弹体设置移动速度、引信和寿命估计;抛物线设置初始高度及上升量,空投弹从飞行高度下落,降落伞则是附着的动画对象。BULLET.CPP:736–805

BulletClass::AI 每帧推进物理位置,追踪弹体在奇数帧更新期望方向,再按 ROT 转向;引信、落地或强制碰撞使它爆炸。低飞弹遇高遮挡会提前爆炸,水下弹离开水面或遇到格子中心附近对象会引爆,对空弹接近目标到 0x80 距离以内可触发近炸。BULLET.CPP:344–485BULLET.CPP:903–954

尤其值得留意:没有被强制引爆的非抛物线、无转向弹体,爆炸时可能把坐标重新放到引信目标点。这是使某类射击表现稳定的专门处理,说明屏幕上的弹道与物理精确碰撞不是同一个设计目标。BULLET.CPP:973–985

5. 普通爆炸如何找到受害者

Bullet_Explodes 对地面目标调用 Explosion_Damage;若目标飞机还在空中,则只在距离足够近时直接对该飞机调用 Take_Damage。不要把地面群伤规则套到空中飞机编队。BULLET.CPP:990–1005

普通地面爆炸先收集中心格及八个邻格的占位对象,使用 IsToDamage 去重,最多收集 32 个。它明确不收集发射者 source,但没有以阵营筛掉所有友军,所以“排除自身”与“免除友军误伤”并不是一回事。随后只处理仍活动、确实在地图上、非 Limbo 且距离小于 1.5 格的对象;中心格实际占据的建筑按距离零处理。COMBAT.CPP:162–237

源码特别说明不能使用显示重叠列表收集目标:是否被选择会改变重叠状态,进而造成联机不同步。这是很好的工程边界案例——伤害取逻辑占位数据,不能取受本地 UI 影响的数据。32 个对象和九格扫描,也使单次爆炸成本有明确上限。

矿石、墙、桥分别走附加逻辑:弹头需有破矿或破墙标志;桥只对特定弹头并经随机强度判断破坏。这些不一定是一个普通“建筑血条”。COMBAT.CPP:239–270

6. 伤害公式及一次完整演算

普通正伤害大体经过以下顺序,注意每层使用整数与定点数,实际结果存在舍入:

text
武器 Damage → 发射者火力倍率 → 步兵卧倒修正(若适用)
→ 受害者及 House 装甲修正 → 弹头对装甲倍率
→ 距离衰减 → 最小/最大伤害限制 → 扣生命值

Modify_Damage 先乘 Modifier[armor],再把距离除以与 SpreadFactor 有关的尺度,得到限制在 0~16 的整数分母;分母非零时做除法。分母小于 4 且先前伤害非零,才保留 Rule.MinDamage 下限;最后限制 Rule.MaxDamage。这不是连续线性衰减,而是分段的整数近似。装甲倍率为零并不会被最低伤害强行救回。COMBAT.CPP:72–128

假设武器 Damage 为 40,所有阵营/对象倍率为 1,对某装甲的 Verses 为 50%,距离换算出的分母为 2,最低/最高伤害限制均未介入。那么原始 40 先变成 20,再除以 2,最终扣 10。若旁边另一单位使用同一种装甲但位于中心,分母为 0,扣 20。若第二单位是铁幕保护目标,TechnoClass::Take_Damage 因倒计时非零而不调用下层伤害;这不是把装甲倍率改为零。TECHNO.CPP:3793–3809

forced 表示绕过部分普通伤害修正及普通免疫,不等于绕过所有派生类逻辑。铁幕判断仍位于调用 ObjectClass::Take_Damage 之前;步兵卧倒修正也在更外层。分析 C4、脚本杀伤时必须沿完整继承调用链阅读。

7. 治疗、死亡和动画也参与规则

治疗复用负伤害。Modify_Damage 对距离极近的目标才返回负值;启用 FIXIT_CSII 时,机械弹头面向非无甲对象,普通治疗面向无甲对象。ObjectClass::Take_Damage 又限制可以接受负伤害的对象类型,并将生命值封顶。因此“Damage=-100”不是任意对象都能用的通用修复按钮。COMBAT.CPP:82–95OBJECT.CPP:1559–1608

基础扣血函数将致命伤害截到当前生命值,结果为零就记录击杀、解除引用,并返回 RESULT_DESTROYED。这里紧邻的一段旧注释声称致命攻击会留一点血,但可执行语句并没有这样做。判断老代码时,注释是线索,函数体才是当前证据。OBJECT.CPP:1614–1665

步兵根据弹头 InfantryDeath 选择枪击、爆炸、火焰、电击死亡;建筑则处理内部对象、残骸、倒塌延时。具有 IsExploding 的对象还会以自身最大生命值和武器弹头启动大范围二次爆炸。这就是同一次扣血能继续引起其他对象死亡、而不仅是把当前对象从列表删除的原因。INFANTRY.CPP:355–420TECHNO.CPP:3811–3835

8. 大范围爆炸:必须保留的源码疑点

Wide_Area_Damage 遍历半径对应的方形格区,对各格调用一次普通 Explosion_Damage,没有在此处以圆形半径剔除角落;一个对象可能被相邻多个局部爆炸重复影响。它不能简化成“每个目标只计算一次到核爆中心的距离”。COMBAT.CPP:393–424

更反直觉的是,代码写的是 rawdamage * Inverse(fixed(cell_radius, dist_from_center))。按当前 fixed 的实现,非中心点的字面比例近似为“距离÷半径”,不是注释描述的随距离增大而减小;中心因构造及除零保护得到满倍率。此处应记录为静态阅读确认的可疑实现,不能擅自把公式改写成符合常识的衰减,再当成原作事实。最终爆炸分布还叠加多次局部爆炸、装甲、范围和边界,需专门运行实验才能完整复现。FIXED.CPP:59–65FIXED.H:117–120FIXED.H:186–199

源码阅读路线与未验证部分

建议按 WEAPON/WARHEAD::Read_INI → Unit/Infantry::Firing_AI → Techno::Can_Fire/Fire_At → Bullet::Unlimbo/AI/Bullet_Explodes → Explosion_Damage → 各类 Take_Damage 顺序追一次攻击,再回头读大范围伤害与特殊单位。特殊攻击衔接见 08 单位与特殊机制

本章没有运行游戏验证命中率、每秒伤害或某发行版平衡表。当前仓库的条件分支也不能直接代表所有 1996 年以后发行版。尤其是大范围伤害异常、极端整数值、对象销毁中的连锁调用与弹体资源不足时的表现,属于值得建立最小实验继续验证的边界。

红色警戒初代 · 源码剖析18 章正文 / 5 份附录
输入关键词,检索 24 份完整文档
全文检索 · 点击结果直达对应小节