# 02｜游戏如何真正运转：启动、时钟、输入与每一帧

本章依据提交 `0dc09bb1d6188d1ec79ab4765b22a48b75ffee32` 的函数实现。这里的“帧”首先指**游戏世界前进一步**，不能直接等同于显示器刷新一次。把这一点和执行顺序读清楚，后面的移动、战斗、联机才有共同的时间坐标。

## 1. 一个程序，有三层生命周期

第一层是进程。`STARTUP.CPP` 用条件编译选择 Windows 的 `WinMain` 或非 Windows 的 `main`；它承担平台准备，再进入 `Main_Game`。Windows 分支建立 `WinTimerClass(60, FALSE)`，非 Windows 分支调用 `Init_Timer_System(60, true)`。因此，不应把全部启动工作都归在 `Main_Game`，也不应把这个仓库理解成只有一种操作系统入口。[平台入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/STARTUP.CPP#L107-L114)、[计时器安装](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/STARTUP.CPP#L350-L361)、[进入游戏主体](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/STARTUP.CPP#L618-L629)。

第二层是一场游戏。`Main_Game` 先执行一次 `Init_Game`，随后在 `while (Select_Game(fade))` 中反复选择模式、进入关卡、结束并返回菜单。第三层才是一帧：每局内部反复调用 `Main_Loop()`，返回值表示是否结束当前循环。[Main_Game](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L215-L262)、[非编辑器循环](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L338-L359)。

| 层次 | 主要入口 | 管理的对象 |
|---|---|---|
| 程序启动 | `main` / `WinMain`、`Init_Game` | 平台、公共资源、类型定义、预分配内存 |
| 进入一局 | `Select_Game`、`Start_Scenario`、`Read_Scenario` | 模式、地图、阵营、实例、任务状态 |
| 推进世界 | `Main_Loop`、`Logic.AI`、`Queue_AI` | 一轮对象行为、待执行命令、时间推进 |

这个分层解释了一个熟悉的体验：换关卡时不必重新启动整个程序，也不必把所有公共资源都重新加载。

## 2. 初始化顺序为什么不能随意调换

`Init_Game` 先做密钥和 `Bootstrap` 初始化，然后处理鼠标、光盘访问和后续 MIX 资源包。真正值得注意的是随后两种“堆”的先后关系：

1. 按 `HOUSE_COUNT`、`UNIT_COUNT` 等枚举数量建立**类型对象池**；
2. 调用各 `*TypeClass::Init_Heap()` 填入已有类型；
3. 读取 `RULES.INI`，调用 `Rule.Process`；部分编译分支还处理 `AFTRMATH.INI`；
4. 最后 `Init_Heaps` 按规则里的 `UnitMax`、`BuildingMax` 等数量建立**实际游戏对象池**。

这说明规则文件处理时，坦克、步兵、建筑的类型对象已经存在；规则是在既有类型框架上设置数据。与此同时，实例池容量依赖规则，必须等规则读取后再分配。源码注释也明确承认类型池先行属于解析器能力限制下的安排。[初始化主体](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L209-L300)、[实例池分配](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2667-L2688)。

公共资源完成后，各子系统还需要 `One_Time`：地图、逻辑、选项、会话和各种类型分别建立自己的长期状态。它与新关卡的 `Init` 不是同一个概念。[Init_One_Time_Systems](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2766-L2788)。

进入关卡时，`Select_Game` 会判断是否已经读入存档；若已加载，就跳过重新 `Start_Scenario`。新关卡经 `Start_Scenario → Read_Scenario` 进入，`Read_Scenario` 清理旧场景，并暂时增加 `ScenarioInit`。`Clear_Scenario` 重置任务计时器、触发器列表、地图单元、对象池使用状态和选择列表。地图此时只做 `Init_Clear`，因为地形主题尚未确定，不能贸然重新加载对应资源。[新关卡与存档分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L1455-L1490)、[场景读取入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L416-L421)、[场景重置](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L695-L768)。

## 3. 三种“时间”，不要混为一谈

源码同时出现 `TIMER_SECOND = 60`、`TICKS_PER_SECOND = 15` 和变量 `Frame`。它们表达不同层次：

| 名称 | 实现含义 | 使用举例 |
|---|---|---|
| 系统计时 tick | 平台定时器的计数，初始化目标为每秒 60 次 | 一帧还需要等待多久、网络超时 |
| 逻辑 `Frame` | 正常完成一轮主循环后递增一次 | 移动、武器冷却、任务状态机 |
| 规则中的“秒” | 以 `TICKS_PER_SECOND = 15` 换算成逻辑帧的名义尺度 | 任务延迟、修复周期、场景倒计时 |

`FrameTimerClass` 直接返回全局 `Frame`；`SystemTimerClass` 返回平台计时器的当前值。两种时基都可以放进 `CDTimerClass<T>`，因此变量名相近并不意味着时基相同。尤其全局 `FrameTimer` 的实际类型是 `CDTimerClass<SystemTimerClass>`：它用系统时间控制帧速率，而不是由逻辑帧自减。[时间常量](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DEFINES.H#L3041-L3050)、[两种时基](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/JSHELL.H#L332-L369)、[FrameTimer 定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GLOBALS.CPP#L745-L747)。

这些倒计时也不是“每帧遍历所有计时器减一”。它保存起点和延迟，需要读取时才计算已过时间，并将剩余值下限夹到零。用 `FrameTimerClass` 的倒计时会随着 `Frame` 推进，用系统时基的倒计时则随真实时间推进。[CDTimerClass::Value](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FTIMER.H#L562-L574)。

### 速度滑块改变的是世界推进速度

普通模式下，`Main_Loop` 用 `Options.GameSpeed` 设置系统倒计时，还按难度加减一个 tick；速度为零时走单独分支。特定多人压缩协议使用 `TIMER_SECOND / Session.DesiredFrameRate`；该分支播放录像时置零，尽快推进。`FIXIT_VERSION_3` 还补了 `DesiredFrameRate` 为零时的防除零处理。[速度控制完整分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2198-L2225)。

举个**用于理解公式的假设值**：正常难度、`GameSpeed=4`、机器足够快，则每逻辑帧目标约为 `4/60` 秒，即 15 帧/秒；若改成 2，则约为 30 帧/秒。同样一个 150 逻辑帧的倒计时，真实时间大致从 10 秒变成 5 秒。这里不是报告默认设置，也没有实测帧率；它只是代码关系的计算示例。

处理本身若已经超过目标时间，`FrameTimer` 会读作零，不再等待。这里没有看到现代常见的“累积真实时间，然后一口气补多个固定步长”的主循环结构。因此“游戏永远固定运行在 15 Hz”是错误概括：15 是规则换算基准和历史目标，实际推进速度还受设置、协议和机器性能影响。

## 4. 一帧到底先做什么

以下是保留关键顺序的**解释性伪代码**，省略调试、平台及部分结算分支：

```cpp
if (!GameActive) return true;
设置本帧系统倒计时;
if (允许本地输入和显示) {
    Map.Input(...);
    Keyboard_Process(...);
    Map.Render();
}
按需记录或恢复视角、选择状态;
按编译分支排序地面显示层;
Logic.AI();
处理消息过期;
Queue_AI();
Call_Back();
处理胜利、失败、重开;
Frame++;
Sync_Delay();
return !GameActive;
```

最容易读错的是：**主循环中的 `Logic.AI()` 在 `Queue_AI()` 之前，常规 `Map.Render()` 又在它们之前。** 不能凭借现代引擎印象，改画成“读输入→执行命令→更新世界→统一渲染”。这里先推进已有状态，再执行本轮到期的玩家事件；这些事件设置的新行为通常交给后续逻辑处理。[输入、渲染及逻辑处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2227-L2267)、[事件与结算检查](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2274-L2308)、[帧计数](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2376-L2380)、[帧末等待](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2449-L2453)。

但这仍不是“每帧只渲染一次”：`Sync_Delay` 等待倒计时到零的过程中，反复做调色板循环、`Call_Back`，以及条件允许时的输入和 `Map.Render()`。所以界面有机会在两个逻辑步骤之间继续响应。此处确认的是调用机会，不能推断每次调用都全屏重画，也不能推断显示刷新率恒定。[Sync_Delay](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2086-L2116)。

还有一个跨系统细节：未定义 `SORTDRAW` 时，地面层排序被放在主循环里、绘制判断之外，注释直接说明是为了保持各机器遍历顺序一致。显示层顺序可能被其他游戏处理使用，它不是完全与模拟隔离的视觉数据。[排序与同步](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2252-L2261)。

## 5. 实例：点一块地，坦克为什么不会在鼠标函数里瞬移

先区分“界面操作”与“改变共同战局的命令”。例如键盘选择下一对象，`Keyboard_Process` 直接取消旧选择、选中新对象、居中地图并设置重绘标记；选取当前视野中的单位也是直接调用地图选择函数。而改变联盟关系则写入 `OutList`。因此这里的设计不是把每一个鼠标位移、每一个按键都广播成战局事件；进入队列的是相应的游戏命令。录像还要额外记录视角和选择，原因也正在于它们不能仅靠移动、攻击等世界事件重建。[本地选择与视角](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L628-L643)、[联盟事件与视野选择对比](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L785-L805)。

假定一辆坦克已经选中，玩家给它普通移动指令：

1. 地图输入系统识别点击含义，最终进入对象的 `Active_Click_With`。`FootClass` 对格子重新调用 `What_Action(cell)`；移动时可能先找相同通行区域附近的可到达位置，然后调用 `Player_Assign_Mission(MISSION_MOVE, TARGET_NONE, destination)`。这里尚未把坦克坐标设成目的地。[FootClass 点击处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FOOT.CPP#L1272-L1318)。
2. `Player_Assign_Mission` 可以马上播放应答语音，再将命令交给 `Queue_Mission`；按住对应排队移动按键时改成 `MISSION_QMOVE`。`Queue_Mission` 把对象、任务、攻击目标、移动目的地封装为 `EventClass`，加入 `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-L248)。
3. 单机、遭遇战使用 `Queue_AI_Normal`，将 `OutList` 搬到 `DoList`，随后执行到期事件；联机与录像由 `Queue_AI` 分流到各自处理路径。因此**单机同样使用命令队列**。[队列分流](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L355-L381)、[单机队列处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/QUEUE.CPP#L406-L442)。
4. `EventClass::Execute` 对命令对象做检查、解除相关联络或队伍关系，再执行 `Assign_Mission`、`Assign_Target` 和 `Assign_Destination`。普通移动清空旧导航队列，排队移动则使用 `Queue_Navigation_List`。[任务事件生效](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/EVENT.CPP#L713-L765)。
5. 下一次适当的对象更新里，车辆接纳新任务；寻路与行驶代码分多帧改变位置，再将相关格子标记需要重画。播放语音、接受命令、开始行驶，是三个不同阶段。

若点击发生在帧首，当前帧的 `Logic.AI` 先按旧状态走一次，然后才处理这个事件；若点击发生在 `Sync_Delay` 内，事件等下一轮 `Queue_AI`。这解释的是源码时序，实际输入到运动的延迟还取决于网络调度、车辆正在做什么、是否允许切换任务，不能笼统声称“所有操作刚好延迟一帧”。

## 6. `AI()` 不只指电脑玩家

`LogicClass::AI` 是世界更新总调度器。它处理的范围包括人类控制的单位、动画、工厂和地图；这里的 `AI` 可理解为“每轮自主处理”。仅把它翻译成“敌方人工智能”，会漏掉大半引擎。

函数中的重要顺序为：

| 次序 | 处理内容 | 对游戏的意义 |
|---|---|---|
| 1 | 场景色彩渐变；全局、桥梁、时间类触发器 | 剧情与任务条件可以在对象更新前改变世界 |
| 2 | 清理临时触发标记；按规则恢复遮罩 | 维持场景状态 |
| 3 | 各 `TeamClass::AI`；时空漩涡等全局现象 | 团队先下达协调动作 |
| 4 | 遍历 `Logic` 中对象，调用虚函数 `obj->AI()` | 单位、子弹、动画等按各自实现更新 |
| 5 | 重算阵营属性；`Map.Logic()` | 整理共享统计和地图逻辑 |
| 6 | 工厂 `AI`；阵营 `AI` | 推进生产、管理阵营行为 |

具体边界以实现为准：前半段见[触发器与队伍调度](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L198-L277)，后半段见[对象、地图、工厂及阵营](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L281-L368)。`FIXIT_VERSION_3` 还调整了多人模式遍历阵营的起点。

这些处理是在同一条显式调用链里顺序进行，不是每个单位一个操作系统线程。单位的 `AI()` 还可能删除自己，因此遍历后比较当前位置是否仍是原对象，变化则 `index--`，防止紧接着的对象被跳过。这体现了**边遍历边修改容器**的设计；不能未经检查就并行化，也不能随意改为现代范围循环。[对象删除后的索引修正](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L281-L324)。

## 7. 每帧更新与任务状态机是两层调度

对象每轮收到 `AI()`，不代表它每轮都重新思考完整任务。`MissionClass::AI` 先调用 `ObjectClass::AI`；对正在空降的步兵、车辆、船只暂停任务处理；只有任务计时器归零且生命值大于零，才按 `Mission` 调用 `Mission_Attack`、`Mission_Move`、`Mission_Harvest` 等虚函数。任务函数返回的整数成为下一次调用前的逻辑帧延迟。[任务调度](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MISSION.CPP#L213-L321)。

`Mission` 是正在执行的任务；`MissionQueue` 是一个待接纳任务槽，不是通用无限命令列表；`Status` 是任务内部状态。`Assign_Mission` 写待执行槽，`Commence` 将它转为当前任务，同时把 `Timer` 和 `Status` 清零。头文件关于“队列保存值加一”的旧注释与当前赋值代码不一致，理解当前版本必须以实现为准。[字段定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MISSION.H#L48-L66)、[接纳与指派](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MISSION.CPP#L343-L391)。

这里的“任务”还有另一个容易混淆的层次：单位 `MissionClass::Timer` 控制行为函数何时再次运行；场景 `Scen.Timer` 统计关卡已经经过的逻辑时间；`Scen.MissionTimer` 则是会显示给玩家、可以触发限时任务条件的全局倒计时。三者虽然都可基于 `Frame`，其所有者与业务含义完全不同。调试“矿车多久重新考虑采矿”和“任务倒计时为什么结束”，应当从不同计时器入口开始。[场景两类时间](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.H#L73-L101)、[单位任务计时器](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MISSION.H#L118-L124)。

车辆 `UnitClass::AI` 在地面、不卸矿、不处于行驶过程且舱门关闭时，才在相应位置尝试 `Commence`。接着调用父类更新，还会处理开火、炮塔旋转、装弹和运输舱门。因此任务的低频决策与车辆每帧运动、武器处理可以共存；这是一种手写状态机和计时器调度，而不是脚本虚拟机为每辆坦克启动一个线程。[车辆更新](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UNIT.CPP#L397-L474)。

## 8. 等待、对话框、结算仍是程序的一部分

`Call_Back` 维护音频回调、主题音乐、语音以及网络或串口服务；它不是 `Logic.AI` 的替代品。等待帧到期时可以继续服务音频和通信，而不让整个世界额外推进。[Call_Back](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L1616-L1639)。

选项等特殊对话框在 `Main_Loop` 外层调用，源码给出的原因是对话框自身可能调用 `Main_Loop`，让游戏在背景继续运行。因此看到“弹窗”不能立即断言模拟已暂停；必须检查具体对话框、模式和调用路径。[特殊对话框安排](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L356-L387)。

胜负也不是任意对象一判断就退出进程。对象与规则处理设置全局结果标记，主循环集中检查并调用 `Do_Win`、`Do_Lose`、`Do_Restart`。退出当前局后，外层淡出、关闭录像文件、按会话类型结束网络连接，并恢复回放相关状态，再返回游戏选择。[结算分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2305-L2358)、[每局清理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L428-L485)。

## 9. 阅读路线与尚未验证的边界

建议依次读：`STARTUP.CPP` 的入口与时钟安装 → `Init_Game` 的类型池/规则/实例池顺序 → `Main_Game` 的内外循环 → `Main_Loop` 与 `Sync_Delay` → `LogicClass::AI` → `MissionClass::AI/Commence` → `UnitClass::AI`。然后带着“这一行发生在第几轮、使用哪个时基、会不会立刻删除对象”三个问题，进入[对象模型与内存](03-object-model-and-memory.md)。

本章确认了源码调用与数据关系，未编译运行原游戏，未测量实际帧率、输入延迟、焦点丢失或所有模式下的暂停表现。`WIN32`、`SCENARIO_EDITOR`、`SORTDRAW`、`FIXIT_VERSION_3`、`FIXIT_CSII` 等宏会改变局部行为；分析某个历史发行版时还需要匹配它实际使用的构建配置。
