# 16　把各模块串起来：四条完整的游戏行为追踪

> 以下是从函数体整理出的静态执行路径，不是一次实测运行的调试日志。路径可以跨越多个逻辑帧；某个分支是否经过，取决于对象种类、状态、规则和编译宏。

## 1. 追踪方法：每一步同时记录五件事

独立理解一个函数还不够。要解释游戏行为，应同时问：入口是什么、读了哪些状态、改了哪些状态、什么时候再运行、失败后走哪里。下面每条路径都按这五个问题展开，而不是把函数名排成一条没有条件的直线。

尤其要分开三种“完成”：输入被识别、命令已经执行、单位完成任务。点击之后发出回应语音，只能说明输入路径到了对应位置；坦克是否已经收到执行阶段的任务、是否能找到路、是否达到目标，是后面的事情。

## 2. 追踪一：选择坦克，点击空地移动

### 2.1 输入先被解释为游戏动作

主循环通过 `Map.Input` 收集输入，再由相应界面与地图处理。进入可移动对象的格子点击处理时，`ACTION_MOVE` 对应移动任务，`ACTION_HARVEST` 对应采矿，`ACTION_ATTACK` 对应攻击格子。同一个格子输入会根据对象能力得到不同含义。[主循环输入阶段](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2230-L2243)、[格子动作映射](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FOOT.CPP#L1269-L1336)。

这里目的地是游戏中的格子目标，并非鼠标的屏幕像素。滚动视口后点击同一屏幕位置，指向的世界位置可以不同；不同分辨率下的屏幕坐标也不能直接作为所有玩家共享的任务目标。坐标转换见[第04章](04-map-coordinates-and-pathfinding.md)，交互选择见[第11章](11-rendering-ui-and-input.md)。

### 2.2 回应语音与排队先于任务完成

`TechnoClass::Player_Assign_Mission` 先按任务选择回应，然后调用 `Queue_Mission`。编队移动带有额外速度数据，按住相应键的普通移动可能转换成 `MISSION_QMOVE`。`Queue_Mission` 将事件加入 OutList，返回是否成功入队。[玩家命令包装](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3244-L3270)、[两种任务入队](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L241-L283)。

因此“听到回应却还没走”不必立即解释成寻路失败。在调用链上，回应、队列调度、任务切换、转向和路径执行本来就是不同阶段。要诊断具体问题，先判断停在哪一个阶段。

### 2.3 执行事件时，重新检查对象是否仍存在

当 `EventClass::Execute` 处理 MEGAMISSION 时，会通过目标表示找出对象，并检查其活动、生命和 Limbo 状态；目标和目的地若解析为已失效的对象，也可能中止执行。这很重要：排队期间世界仍在变化，不能认为点击时有效的对象在执行时必然还有效。[MEGAMISSION 对象检查](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L663-L710)。

接下来可能解除 Radio 协作、从 Team 移除、通过 `Assign_Mission` 提交待切换的 `MissionQueue`、清理挂起任务；普通任务设置 TarCom 与 NavCom，排队移动则进入导航列表。`Commence` 才把待处理任务切换为当前 Mission，并重置相应任务状态。[任务、目标与目的地赋值](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L716-L765)、[待处理任务与切换](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MISSION.CPP#L343-L391)。

此时坦克收到的是“往那里移动”的意图，不是瞬间到达终点。部分准备不必等下一轮：`DriveClass::Assign_Destination` 可以同步调用 `Start_Of_Move`，进一步尝试 `Basic_Path`。后续对象逻辑继续推进任务、转向与位移，并反复检查动态障碍、维护地图状态。应把事件阶段的准备与以后多帧的持续行驶分开。[目的地分配可立即启动准备](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DRIVE.CPP#L618-L627)。

### 2.4 准确的跨帧时序

主循环在常规路径先渲染，再运行 `Logic.AI`，之后运行 `Queue_AI`。所以不能画成“输入一产生，在本帧对象 AI 之前必然执行”。这还会受到联机调度、录像和等待阶段影响。[实际阶段顺序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2230-L2303)。

| 观察点 | 需要看的状态 | 能证明什么 |
|---|---|---|
| 点击解释后 | Action、对象类型、格子目标 | 操作被解释成移动还是其他动作 |
| 入队后 | Event、Whom、Mission、Destination | 命令被表达并进入安排流程 |
| Event 执行后 | MissionQueue、Mission、NavCom、导航列表 | 待切换任务和目的地接收了命令；当前任务是否切换还要看 Commence |
| 移动推进时 | 路径、方向、占格、可进入结果 | 世界尝试执行移动 |
| 目标附近 | 任务状态、目的地是否合法 | 移动完成、重新规划或放弃 |

这张表也是调试卡单位问题的顺序。先查导航状态再查地图；不应一见到“没有移动”就把所有问题归给寻路算法。

## 3. 追踪二：坦克向敌方建筑开炮

### 3.1 攻击命令只设置了目的，不保证立即发射

对对象的攻击动作调用 `Player_Assign_Mission(MISSION_ATTACK, object->As_Target())`。对象目标与强制攻击地面格子有不同语义；目标对象可以移动或被销毁，而固定地面坐标不会跟随另一个对象。[对象攻击入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FOOT.CPP#L1197-L1208)。

在车辆自己的 `Firing_AI` 中，首先要求 TarCom 合法并存在主武器，然后选择武器，调用 `Can_Fire`。返回 FIRE_OK 才发射；若是 FIRE_FACING，就尝试改变车体或炮塔方向；若是 FIRE_CLOAKED，则解除隐形。这段代码直接解释了为什么下达攻击后，单位可能先转炮塔、先显形而没有立刻扣敌人的生命。[车辆开火决策](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UNIT.CPP#L643-L688)。

### 3.2 发射行为与弹体行为分开

通用 `TechnoClass::Fire_At` 负责发射层面的工作，具体 Unit 版本还可能补充特殊单位效果。产生的 Bullet 对象拥有后续飞行和命中生命周期。不要把“Fire_At 被调用”等同于“指定敌人必然承受一次完整伤害”。[通用发射入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3023-L3195)、[车辆发射扩展](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UNIT.CPP#L4206-L4250)。

炮弹具有自己的目标、位置、移动与引信。它可能经过特殊直接伤害路径，也可能在爆点进入范围伤害。普通地面爆炸和对空目标不能简单套同一套候选对象搜索。具体差异和条件见[第06章](06-combat-and-damage.md)。

### 3.3 伤害不是一个唯一公式

`Modify_Damage` 处理弹头对护甲的 Modifier，按距离和 SpreadFactor 缩放，并处理最小、最大伤害。负伤害还存在治疗对象筛选。外层 `Take_Damage` 的具体对象实现继续处理保护、修正与死亡。因此一个“基础攻击40”的例子不能直接推出“打420血单位固定11发”。[基础伤害变换](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L72-L128)。

为了观察一次炮击，建议记录以下字段而不是只打印最终血量：使用哪个武器、弹头、弹体；射击者火力相关倍率；目标护甲与保护；弹体爆点到目标距离；每次 Take_Damage 的输入和输出；是否真的进入死亡分支。这个跟踪粒度能区分武器配置问题、命中问题、伤害修正问题和死亡表现问题。

### 3.4 销毁牵动世界关系

一个建筑消失，其他单位可能仍把它当目标，UI 可能仍选中它，地图仍有它占用的格子，AI 的建筑统计也可能引用它。对象离开世界时，`Limbo` 会取消选择、解除跟踪、撤销地图标记，从绘制层和相应逻辑集合移除，再标记为 Limbo。[基础对象离场](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1340-L1368)。

派生类还会补充自身清理。**生命归零、播放爆炸、离开地图、返还池槽是有关联但不应混写成同一个操作的阶段。**设计自己的游戏时，同样要考虑全部引用者；只从绘制列表删掉一个精灵，会留下“看不见但仍挡路”的幽灵对象。

## 4. 追踪三：从采矿到生产完成，再到战场出现

### 4.1 资金是世界状态，侧栏数字只是表现

矿车的采集与卸载由 Unit 的任务状态机和 Radio 协作处理，最终修改 House 资金相关状态。卸矿调用位置与旧建筑卸料函数不能仅按名字混同。[第07章](07-economy-production-and-buildings.md)给出已核查的实际结算路径。

这里的工程意义是：生产系统需要查询 House 的可用资金，而不是读取侧栏文本。否则无界面模拟或联机重放就无法稳定复现生产结果。

### 4.2 点击图标形成 PRODUCE 事件

当 Event 执行 PRODUCE 时，按所属 House 调用 `Begin_Production(type,id)`；暂停、放弃和放置也分别是事件类型。建造界面并没有在任意一次鼠标回调里直接把建筑插入地图。[生产相关事件分派](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L617-L644)。

Factory 的 `Set` 首先放弃旧内容，重置阶段与暂停状态，然后调用类型的 `Create_One_Of` 创建生产对象，保存余额和购买价格。因此“尚未生产完成”的东西已经可能拥有运行期对象，只是未进入战场。这再次说明“对象已分配”与“地图上存在”不同。[Factory 设置生产对象](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FACTORY.CPP#L290-L329)。

### 4.3 每次生产推进既改进度，也处理付款

Factory 被世界逻辑集中推进。其 AI 在未暂停、存在生产对象或特殊项目时，依据阶段计时推进；计算本次成本并与可用资金比较。钱不够会把刚推进的阶段退回，钱够则扣款并减余额；到达 STEP_COUNT 后暂停并结清余款。[Factory 的周期调度](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L330-L337)、[阶段与扣款](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FACTORY.CPP#L201-L239)。

这可以解释生产不是“一开始一次性扣清、最后播放动画”的简单定时器。价格、剩余余额、阶段、计时速率、电力和工厂条件共同决定进展。具体公式与取消退款见[第07章](07-economy-production-and-buildings.md)。

### 4.4 完成不等于放置成功

建筑完成后还要选合法位置；单位完成后还涉及生产建筑出口。最终的 `ObjectClass::Unlimbo` 在普通非 `ScenarioInit` 路径检查能否入格，修正坐标，落下地图标记，提交到绘制层，并为有逻辑活动的对象提交到 Logic。场景初始化标志可以绕过这一层入格检查；整个入场操作仍可能因其他条件失败。[基础对象入场](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1391-L1420)。

所以排查“已经生产好但没出现”时，需依次看 Factory 完成标记、交付状态、建筑出口或摆放位置、Can_Enter_Cell、Mark 与 Unlimbo 的返回值。单看工厂阶段已满不足以证明单位已进入世界。

## 5. 追踪四：破坏目标建筑，触发增援或胜利

场景初始化建立 TriggerType 配置与 Trigger 实例，关联到逻辑、阵营、对象或地图位置等来源。触发器等待条件满足，再执行 Action。与玩家输入的 `EventClass` 相比，关卡条件的 `TEventClass` 是另一套概念，名字相近但用途不同。

在世界逻辑入口，LogicTriggers 按需检查全局标志变化、桥梁变化、时间和任务计时器。触发器发生在主对象更新之前，某些本轮稍后发生的变化因而可能在后续阶段或下一轮才被相应检查观察到；对象事件又可以走自身触发路径，不能把所有条件都视为一次统一轮询。[Logic 触发器检查](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L198-L253)。

条件组合可能包含已经发生并锁存的事件，也可能包含每次重新求值的时间或全局状态。读关卡时必须分清“一次发生过”与“当前仍成立”。两个条件同时写在触发器里，也不能自动解释成“第一个发生后才开始第二个的倒计时”。具体锁存和重置见[第10章](10-scenarios-triggers-and-campaign.md)。

被摧毁通知的具体入口也能闭合这条链：致命伤害进入 `Record_The_Kill`；Techno 的对应实现向关联触发器发送 `TEVENT_DESTROYED`；`TriggerClass::Spring` 检查条件并执行动作；胜负或增援动作随后进入对应处理。[致命伤害通知](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1637-L1647)、[摧毁事件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3886-L3901)、[触发动作执行](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L305-L322)。

`Flag_To_Win` 首先设置阵营的 IsToWin 和 BorrowedTime。在普通战役路径，还要等 House 更新检查延时及 Blockage，才设置 PlayerWins。因此触发“胜利动作”与立即弹出胜利画面之间仍有流程。[胜利申请](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/HOUSE.CPP#L4062-L4070)、[阵营转为最终标志](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/HOUSE.CPP#L938-L959)。

主循环最后检查 PlayerWins、PlayerLoses 或 PlayerRestarts，转入 `Do_Win`、`Do_Lose`、`Do_Restart`。过关后可能继续评分、影片、下一场景等逻辑，而非在伤害函数里直接结束进程。[主循环胜负出口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2308-L2357)。

## 6. 把四条路径拼成系统图

```mermaid
flowchart TD
  I[玩家输入] --> Q[命令队列]
  Q --> E[事件执行]
  E --> U[单位任务]
  E --> F[生产状态]
  F --> U
  U --> M[地图与对象状态]
  M --> T[关卡条件]
  T --> A[关卡动作]
  A --> U
  A --> V[胜负与场景切换]
  M --> P[图形与声音]
```

此图表示因果依赖，不能替代主循环时序。它最值得学习的地方是反馈：单位改变地图，地图影响单位；关卡观察世界，也会继续向世界增加对象和命令；生产与战斗共同消耗并改变阵营资源。

## 7. 实践阅读路线与边界

挑一种你熟悉的操作，只沿对应路径读一遍，并写一张“进入前/执行后”状态表。先完成空地移动，再做普通炮击，再做有条件分支的矿车或运输装载，最后追超武。这样每次只增加一类复杂度。

本文没有给出真实帧号、网络时延、战斗耗时或录像兼容性结论。需要运行才能回答这些问题。已确认的是路径中的分派、状态更新与检查关系；真实游戏资源、宏选择及运行期条件决定一次具体操作最终经过哪些分支。
