# 10 关卡、触发器与战役：怎样用数据编排一场战争

一关红警并不是一段专门编译的 C++ 程序。引擎读取地图、阵营、单位、队伍配方、触发器和影视配置，再用通用逻辑将它们组合成一场有开场、目标、援军、转折与结局的游戏。与此同时，源码确实保留了特定战役和资料片的分支；这是一套大量依靠数据驱动、又允许程序补丁解决特殊需求的实际生产系统。

本章以读取函数和事件执行函数为依据。没有运行原版关卡，下面的教学案例也不是冒充官方地图的复原记录。

## 1. 关卡文件是对象图的配方，加载顺序就是依赖顺序

入口关系是 `Start_Scenario → Read_Scenario → Read_Scenario_INI → Fill_In_Data`。读取关卡前清理旧场景，在 `ScenarioInit` 状态下构造对象；读取失败会回报错误。`ScenarioInit` 是多处递增、递减的初始化控制计数，不能只当作普通开关看待，因为对象放置期间有些常规运行限制需要暂时改变。[读取总入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L416-L500)、[INI 读取](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L2098-L2115)。

`Read_Scenario_INI` 先恢复基础规则；启用相应资料片宏时再应用 `AftermathINI`；随后用当前地图 INI 覆盖 General、AI、Land、Objects、Difficulty 等规则。因此关卡作者可以在一张地图内改变单位或规则，而不必重新编译全游戏。`Read_Scenario` 外层在特定多人/资料片条件下还可能再应用 `MPLAYER.INI`，所以“地图规则永远是最后一层”也不是跨全部模式成立的结论。[规则叠加](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L2170-L2202)、[额外多人规则层](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L422-L485)。

主要内容按下面次序建立：

| 顺序 | 内容 | 为什么在这里读取 |
|---|---|---|
| 1 | Basic 元信息 | 影片、玩家阵营、格式版本、跨关继承控制 |
| 2 | Houses | 所有归属关系先有目标 |
| 3 | TeamTypes | 触发动作可以引用队伍类型 |
| 4 | PlayerPtr | 对象创建时已知本地玩家 |
| 5 | Trigs | 放置单位和格子时能绑定触发器 |
| 6 | Map、Waypoints、CellTriggers、MapPack | 地图尺寸、剧场、路径点与底图 |
| 7 | Terrain、Units、Ships、Infantry、Structures | 构造并摆放对象 |
| 8 | Base、Overlay、Smudge、Briefing | 基地计划、地面覆盖物、污迹和简报 |

这个顺序不是说明性伪码，而是从调用次序归纳；例如队伍类型先于触发器，正是为了解决后者引用前者的问题。源码先尝试 `MISSION.INI` 中该关的简报文本，若为空再取地图中的 Briefing；不能反过来描述它们的覆盖优先级。[场景构建次序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L2218-L2365)。

## 2. 地图为什么既是 INI，又包含压缩二进制

`DisplayClass::Read_INI` 读取可玩矩形和 Theater，再初始化对应地形、单位、建筑、弹体与动画图像。它逐个加载 Waypoints；CellTriggers 则把“格编号→触发器类型”变成运行时引用，同时增加附着计数。[地图尺寸与剧场](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L4220-L4288)、[路径点与格触发器](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L4290-L4335)。

大量地砖不逐行列为 `cell=tile`。`MapPack` 经 `Get_UUBlock` 读出字节块，再由 `Map.Read_Binary` 接上 `LCWStraw` 解压。较新格式先读整张图的模板编号，再读整张图的图块子编号；旧格式逐格交错读取两者。这种分离排列可以让相似字段靠在一起，利于压缩，同时保持关卡作为一个文本文件流转。[MapPack 入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L4337-L4345)、[压缩读取与格式分支](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAP.CPP#L898-L969)。

文本读完并不等于场景可以马上开始。`Fill_In_Data` 重新计算可建造列表、移动区域、触发器分类表、地图边缘可见状态，创建跨关继承对象，统计胜利阻挡条件与未毁桥数，最后让对象观察周围。这是“存配置、运行时重建派生数据”的典型做法，减少了文件中相互矛盾的冗余信息。[派生状态重建](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L519-L641)。

## 3. 触发器是两条事件、两条动作和一份运行状态

`TriggerTypeClass` 存配置：持久性、所属方、事件组合方式、动作组合方式、Event1/2 与 Action1/2。`TriggerClass` 存实例状态：动态事件数据、计时器、是否已触发、附着计数以及格位置。`Find_Or_Make` 会查找已有的同类型实例，否则创建。因此多个物体或格子引用同一触发器类型时，可能共享一个运行中的触发器；不能默认“每个格都拥有独立计时器”。[触发器实例初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L128-L136)、[实例复用](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L453-L467)。

`TriggerTypeClass::Fill_In` 真实读取的头部字段是：持久类型、House、EventControl、ActionControl；随后两次读事件、两次读动作。新格式事件包含事件号、Team 引用、Data；动作包含动作号、Team 引用、Trigger 引用、Data。文件上方某些旧注释仍写成早期简化字段，不能拿来当当前格式说明。`NewINIFormat` 会改变解析方式，旧版字符串引用还有后续修复步骤。[触发器字段顺序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGTYPE.CPP#L1935-L1952)、[事件格式](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TEVENT.CPP#L506-L543)、[动作格式](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TACTION.CPP#L247-L271)。

## 4. 事件有的靠通知，有的靠查询，而且并非全都锁存

事件输入从多个地方来。单位完成格级处理时，向本格触发器发送 `TEVENT_PLAYER_ENTERED`，同时检查横线、纵线及移动区域进入；这里明确排除了处于 Cloaked 状态的单位。House 自己检查经济、建筑、生产等条件；通用逻辑定期检查计时器、全局标志和桥梁状态。[单位触发入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FOOT.CPP#L1406-L1454)、[通用事件轮询](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L209-L253)。

`TEventClass::operator()` 先看动态 `IsTripped`。一旦某个事件已锁存，后续检查可直接为真；进入格子等事件会验证对象所属方，再设锁存。于是组合条件可能表达“曾经进入过这里，并且另一个条件后来成立”，不要求两者必须同一帧发生。但时间、全局标志和任务倒计时分支直接判断当前状态返回，并没有都写 `IsTripped=true`。**AND 不等于所有条件永久记忆，更不等于所有条件同帧出现。** 必须逐类读事件实现。[锁存、全局条件与进入校验](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TEVENT.CPP#L220-L303)。

“敌军全灭”也不是遍历全部可见精灵再肉眼计数。例如 `TEVENT_ALL_DESTROYED` 使用 ActiveBScan、ActiveUScan、ActiveIScan、ActiveVScan 的按位或；建筑存在、特定新造单位、资金达到阈值则分别用对应统计。事件名称容易给人一种无限普遍的含义，真实判断范围应以函数里包含哪些扫描字段为准。[阵营事件条件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TEVENT.CPP#L318-L459)。

`TriggerClass::Spring` 组合结果后执行动作：ONLY 仅检查事件一；AND 要两个都成立；OR 任一成立；LINKED 也可由任一事件启动，但事件一对应动作一、事件二对应动作二。普通双动作按 ActionControl 决定调用一个还是两个。[组合与动作分派](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L230-L324)。

## 5. 一次性、半持久、持久：触发后的生命周期

| 模式 | 关键行为 | 适合表达的关卡意图 |
|---|---|---|
| `VOLATILE` | 有动作成功后脱离引用并删除 | 一次伏击、一次完成提示 |
| `SEMIPERSISTANT` | 每次满足时解除当前附着并减少计数，仍有附着则暂不执行最终动作 | 一组对象全部满足后才行动 |
| `PERSISTANT` | 执行动作后重置动态事件状态 | 周期增援、可重复机关 |

这个生命周期与动作返回值有关：至少一个动作报告成功，才进入删除或重置流程；援军动作会把 `Do_Reinforcements` 的结果向上传回。因此“条件达成”与“该触发器已经完成”不是绝对等价的事。[附着计数、成功条件与重置](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L270-L353)。

半持久尤其不能理解成“触发两次”。计数来自附着对象或格子的数量，逻辑会在事件完成时解除该处绑定。若同一触发器横跨多种附着方式，或者动作自己销毁对象，还需结合具体实例的附着状态分析。引擎中触发器可在执行中删除自己，调用者随即检查对象是否仍有效；这是很多 `if (!IsActive) return` 的实际原因。

## 6. 完整推演：一分钟后，玩家到达路口，装甲援军出现

设计一个教学触发器 R：一次性；事件一为玩家进入路口格；事件二为 Elapsed Time，Data=10；组合 AND；动作是给指定 TeamType 发援军。再给队伍设置从某侧入场、移动至 waypoint B 的脚本。此处是语义配置，不提供未经运行验证的数字枚举 INI。

1. 地图加载 CellTriggers 时调用 `Find_Or_Make(R)`，生成实例；构造函数对两事件 Reset。Elapsed Time 的计时器设置为 `Data * (TICKS_PER_MINUTE/10)`，所以 10 表示一分钟逻辑时间。
2. R 同时具有格事件和通用时间事件，派生数据阶段将相应实例列入通用触发器表。玩家三十秒时进入路口，事件一锁存；事件二仍假，不发援军。
3. 一分钟逻辑时间到，通用事件检查再次调用 Spring，事件一因锁存为真、时间条件也真，动作才执行。如果玩家到一分半才到路口，则到达时可以立即执行。
4. 动作调用 `Do_Reinforcements`，创建队伍和其成员，决定地图入口并放置，接着由 Team 脚本接管成员移动。
5. 动作报告成功后，R 脱离引用并删除，不会在下一轮再发同一波援军。

最容易弄错的是时间原点：**这里不是“进入路口后再等一分钟”**，而是从触发器初始化或重置时开始计时。若需要进入后才启动倒计时，要用后续触发器、全局状态或任务倒计时动作重新表达因果。[事件计时器 Reset](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TEVENT.CPP#L183-L188)、[按事件决定附着类型](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TEVENT.CPP#L677-L764)。

还要区分 Elapsed Time 和界面上的 `Scen.MissionTimer`。后者有开始、暂停、加时、减时、设定等动作；设定动作会启动计时器，任务到时事件要求计时器仍在活动且已为零。通用逻辑在检查完到期触发器后再停表，避免本帧尚未检查的到期事件失去机会。[任务倒计时操作](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TACTION.CPP#L470-L515)、[到期检查与停表](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LOGIC.CPP#L235-L253)。

## 7. 援军和“创建队伍”为什么不是同一个按钮

`TACTION_CREATE_TEAM` 创建 Team 逻辑实例，通常让它按配方招募已有单位；`TACTION_REINFORCEMENTS` 则进入 `_Create_Group` 真正创建成员对象。两者都引用 TeamType，但一个偏向组织兵力，一个同时负责把新的兵力带入场景。[两种动作](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TACTION.CPP#L652-L673)。

`_Create_Group` 创建并强制激活队伍，按成员类型与数量造出对象、加入团队，区分运输载具和乘客，并处理部分飞机作为临时援助单位的标记。`Do_Reinforcements` 随后处理纯步兵从建筑出来的特殊情况，否则按 House 的边缘设置和 Origin waypoint 计算入场格，调用 Unlimbo 让对象离开未放置状态。入场失败时尝试相邻地图外位置，仍无法处理的对象会走清理分支。援军不是绕过对象系统的一团画面效果。[成员创建与运输归组](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/REINF.CPP#L167-L250)、[入场选择与放置](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/REINF.CPP#L372-L519)。

还有一个接口层面的边界：常规入场路径在函数末尾直接返回 true，并没有检查“所有预定成员均已成功放置”。因此动作报告成功足以使一次性触发器完成，却不能当作援军完整到场的证明。调试这类问题必须同时检查创建数量、Unlimbo 结果与场上对象数。[入场结果返回](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/REINF.CPP#L509-L529)。

这种复用使同一套队伍脚本能服务空投、地图边缘进攻、建筑出兵和救援。相应地，设计关卡时除了数量，还要检查运输容量、入口可达性、地图边界以及后续脚本，否则“触发了援军”不代表玩家一定能在预期位置看见所有成员。[小队脚本机制](09-ai-teams-and-missions.md)接续解释入场之后的控制。

## 8. 胜利从一个标记变成电影、结算与下一关

`TACTION_WIN/LOSE` 先把指定阵营与玩家比较，再调用玩家 House 的 `Flag_To_Win/Lose`。这些函数设置延迟与状态；House 更新时才在 BorrowedTime 到零、胜利阻挡条件许可时写入 `PlayerWins/PlayerLoses`。主循环随后消费标记，调用 `Do_Win/Do_Lose`。所以最后目标被毁后稍等片刻才宣布任务完成，是流程中明确存在的阶段。[胜负动作](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TACTION.CPP#L597-L619)、[延迟标记](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/HOUSE.CPP#L4062-L4099)、[House 结算条件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/HOUSE.CPP#L938-L959)、[主循环消费结果](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L2305-L2346)。

`Allow Win` 更特殊：加载阶段统计相关触发器增加 House 的 `Blockage`，而触发器销毁时有专门逻辑减少阻挡并重设计时。这份实现存在值得进一步核验的不对称：初始化统计第一动作及有效的第二动作，析构处则只检查 Action1。讲解时应保留这个边界，不能凭名称断言把 Allow Win 放在任意动作槽都完全等价。[阻挡初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L609-L620)、[析构解除阻挡](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TRIGGER.CPP#L175-L178)。

`Do_Win` 对多人游戏转入多人结算后结束；战役则播放胜利影片、视配置显示分数、处理一次性关卡及最终结局，再进入地图选择或特别的下半关分支。它可以记录资金、建筑、车辆、步兵、舰船，形成 Carryover 链；下一关声明继承时再重建这些对象。资金还可按百分比和上限结转，任务计时器也有独立继承控制。跨关延续因此是有选择的状态导出与重新构造，不是让整个旧世界一直留在内存里。[胜利后的战役分流](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L849-L950)、[跨关对象与计时器](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L952-L1047)。

## 9. 编辑器不是另一套游戏世界

仓库中 `MAPEDIT.CPP` 的主体受 `SCENARIO_EDITOR` 控制。编辑器复用地图、对象、任务、队伍及触发器类型，有调试状态下的游戏/编辑切换逻辑；TeamType 编辑界面直接修改脚本表和成员配置。写关卡的 `Write_Scenario_INI` 又受 `CHEAT_KEYS` 条件控制，并先载入旧文件以保留部分手工维护的内容。[编辑器编译边界](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAPEDIT.CPP#L61-L79)、[编辑切换](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MAPEDIT.CPP#L430-L472)、[关卡保存入口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SCENARIO.CPP#L2450-L2484)。

这体现了一种很务实的制作流程：程序员提供可组合的事件、动作和对象，关卡作者通过数据组织流程，编辑工具直接操纵同一模型。工具代码存在并不证明任意发行版默认开放完整编辑功能；具体可用性仍由构建宏、调试开关和资源决定。

## 源码阅读路线与待验证边界

按 `Read_Scenario_INI → DisplayClass::Read_INI → Fill_In_Data → TriggerTypeClass::Fill_In → TEventClass::operator() → TriggerClass::Spring → TActionClass::operator() → Do_Reinforcements → Flag_To_Win → Do_Win` 阅读，可以把“一张 INI”一直追到“下一关开始”。

需要运行验证的重点包括：特定版本 INI 的兼容性、半持久复杂附着计数、Allow Win 第二动作槽的表现、援军入口失败清理以及真实战役的跨关继承。本文没有用臆造地图冒充官方关卡实例，也没有把条件编译的特殊关卡补丁解释为所有关卡的通用规则。
